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
structkeyword, 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.
- The
-
Move Semantics
- The struct update syntax moves data from the original instance.
- If a field is moved (e.g.,
Stringinusername), the original instance becomes invalid for that field. - Fields implementing the
Copytrait (likeboolandu64) 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
ColorandPoint):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
ColorandPointhave the same types (i32), they are distinct types. -
Functions that take a
Colorcannot take aPoint, 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
Userstruct, we usedString(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
&strreferences: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
DisplayorDebug, 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 tostderr.
#[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, ortrait. - 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
areais defined inside theimplblock forRectangle. -
&selfrefers to the current instance ofRectangleand is shorthand forself: &Self. -
selfcan be used to borrow immutably (&self), mutably (&mut self), or to take ownership (self).- In this case,
&selfis used to borrow the instance without taking ownership.
- In this case,
-
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
widthfield):
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 thewidthmethod, whilerect1.widthaccesses 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:
selfrefers to the callingRectangleinstance.other: &Rectangleis an immutable reference to another rectangle.self.width > other.width && self.height > other.heightchecks ifselfcan holdother.
-
Expected Output:
Can rect1 hold rect2? true Can rect1 hold rect3? false
Associated Functions
- Associated functions don’t require
selfand 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:
Selfrefers to theRectangletype.Rectangle::square(3)creates a square with width and height both set to3.
Multiple impl Blocks
- You can split methods across multiple
implblocks for the samestruct.
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).