Skip to Content
RustBooksThe Rust BookUsing Structs to Structure Related Data

Using Structs to Structure Related Data

5.1 Defining and Instantiating Structs

  • Structs vs Tuples

    • Both can hold multiple related values.
    • Structs are more flexible because fields are named, unlike tuples, where order matters.
  • Defining a Struct

    • Use the struct keyword, followed by a name and fields inside curly brackets.

      struct User { active: bool, username: String, email: String, sign_in_count: u64, }
  • Creating a Struct Instance

    • Use the struct name and curly brackets with key-value pairs.

    • Order of fields doesn’t matter.

      fn main() { let user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; }
  • Accessing Struct Fields

    • Use dot notation (user1.email) to access specific values.

    • If mutable, values can be updated via dot notation.

      fn main() { let mut user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
  • Returning a Struct Instance from a Function

    • Struct can be returned as the last expression in a function.

      fn build_user(email: String, username: String) -> User { User { active: true, username: username, email: email, sign_in_count: 1, } }
  • Field Init Shorthand

    • If parameter names match struct field names, you can omit the explicit field names.

      fn build_user(email: String, username: String) -> User { User { active: true, username, // no need to write username: username, email, // no need to write email: email, sign_in_count: 1, } }

Creating Instances from Other Instances

  • Sometimes you want to create a new struct instance that reuses most values from an existing instance while changing a few fields.

  • Without Struct Update Syntax

    • You can manually specify fields from an existing instance for the new one.

      fn main() { let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; }
  • Using Struct Update Syntax

    • The .. syntax is used to copy all remaining fields from another instance.

    • It reduces code redundancy.

      fn main() { let user2 = User { email: String::from("another@example.com"), ..user1 }; }
  • Important Notes

    • The .. syntax must come last in the struct definition.
    • Fields can be set in any order, regardless of their original order in the struct.
  • Move Semantics

    • The struct update syntax moves data from the original instance.
    • If a field is moved (e.g., String in username), the original instance becomes invalid for that field.
    • Fields implementing the Copy trait (like bool and u64) are simply copied, and the original instance remains valid for those fields.

Tuple Structs

  • Similar to tuples but with a unique name to distinguish types.

  • Fields are not named, only their types are specified.

  • Useful when naming each field would be redundant or verbose.

  • Example (defining and using tuple structs Color and Point):

    struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
  • Even though both Color and Point have the same types (i32), they are distinct types.

  • Functions that take a Color cannot take a Point, even if both contain the same data types.

  • Accessing Tuple Struct Fields

    • You can destructure tuple structs or use dot notation with an index to access fields.

    • Example:

      let black = Color(0, 0, 0); let red_value = black.0; // Accessing the first value

Unit-Like Structs

  • Structs with no fields, similar to the () unit type.

  • Useful when you need to implement traits but don’t need to store any data.

  • Example (declaring and using a unit-like struct AlwaysEqual):

    struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
  • Unit-like structs can be instantiated without curly brackets or parentheses.

Ownership in Structs

  • In the User struct, we used String (an owned type) instead of &str (a borrowed string slice).

  • This ensures that each instance owns its data, and the data remains valid for the lifetime of the struct.

  • Storing References in Structs

    • It’s possible to store references in structs, but doing so requires specifying lifetimes.
    • Lifetimes ensure that referenced data stays valid as long as the struct does.
  • Example of Invalid Struct with References

    • The following code won’t compile because it lacks lifetime specifiers for the string references:

      struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }
  • Compiler Error Explanation

    • The compiler requires lifetime specifiers for &str references:

      error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter
  • Fixing the Error (Introducing Lifetime Parameters)

    • The error can be fixed by adding a lifetime specifier, like 'a, to the struct definition:

      struct User<'a> { active: bool, username: &'a str, email: &'a str, sign_in_count: u64, }

5.2 Example

struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }

Adding Debugging Capabilities with the Debug Trait

  • By default, structs don’t implement Display or Debug, which means they can’t be printed easily.
  • Using #[derive(Debug)] lets us print the struct using the {:?} or {:#?} specifiers.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }

Using the dbg! Macro for Debugging

  • The dbg! macro helps print expressions with context (file, line number) and outputs to stderr.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
  • Output Example:

    [src/main.rs:10:16] 30 * scale = 60 [src/main.rs:14:5] &rect1 = Rectangle { width: 60, height: 50, }

5.3 Defining Methods on Structs

  • Methods are similar to functions but are defined within the context of a struct, enum, or trait.
  • The first parameter of a method is always self, representing the instance of the struct the method is called on.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
  • The method area is defined inside the impl block for Rectangle.

  • &self refers to the current instance of Rectangle and is shorthand for self: &Self.

  • self can be used to borrow immutably (&self), mutably (&mut self), or to take ownership (self).

    • In this case, &self is used to borrow the instance without taking ownership.
  • Methods are called using dot notation:

    rect1.area() // Calls the area method on rect1

Naming Methods the Same as Fields

  • You can define methods with the same name as a field.
  • Rust differentiates between a method and a field based on whether parentheses are used.
  • Example (method with the same name as the width field):
impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
  • rect1.width() calls the width method, while rect1.width accesses the field.
  • Methods named the same as fields are often used as getters, allowing controlled access to private fields.

Implementing Methods with Multiple Parameters

#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
  • Explanation:

    • self refers to the calling Rectangle instance.
    • other: &Rectangle is an immutable reference to another rectangle.
    • self.width > other.width && self.height > other.height checks if self can hold other.
  • Expected Output:

    Can rect1 hold rect2? true Can rect1 hold rect3? false

Associated Functions

  • Associated functions don’t require self and are called using the :: syntax.
  • These functions are often used as constructors (e.g., a function to create a square Rectangle).
impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); println!("{:?}", sq); }
  • Explanation:
    • Self refers to the Rectangle type.
    • Rectangle::square(3) creates a square with width and height both set to 3.

Multiple impl Blocks

  • You can split methods across multiple impl blocks for the same struct.
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } }
  • Note: There’s no specific reason to split methods here, but it’s useful when combining different traits or generic implementations (covered in later chapters).
Last updated on