9.2 Defining, Instantiating, and Accessing Structs
Defining and using structs in Rust involves declaring the structure type and then creating instances using struct literal syntax.
9.2.1 Struct Definitions
The general syntax for defining a named-field struct is:
struct StructName {
field1: Type1,
field2: Type2,
// additional fields...
} // Optional comma after the last field inside } is also allowed
Here, field1
, field2
, etc., are the fields of the struct, each defined with a name: Type
. Field definitions listed within the curly braces {}
are separated by commas (,
).
A comma is permitted after the very last field definition before the closing brace }
. This trailing comma is optional but idiomatic (common practice) in Rust for several reasons:
- Easier Version Control: When adding a new field at the end, you only need to add one line. Without the trailing comma, you’d have to modify two lines (add the new line and add a comma to the previously last line), making version control diffs slightly cleaner.
- Simplified Reordering: Reordering fields is easier as all lines consistently end with a comma.
- Code Generation: Can simplify code that automatically generates struct definitions.
- Consistency: Automatic formatters like
rustfmt
typically enforce or prefer the trailing comma for consistency.
Concrete examples:
struct Point {
x: f64,
y: f64, // Trailing comma here is optional but idiomatic
}
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64, // Trailing comma here too
}
- Naming Convention: Struct names typically use
PascalCase
, while field names usesnake_case
. - Field Types: Fields can hold any valid Rust type, including primitives, strings, collections, or other structs.
- Scope: Struct definitions are usually placed at the module level but can be defined within functions if needed locally.
9.2.2 Instantiating Structs
To create an instance (instantiate) a struct, use the struct name followed by curly braces containing key: value
pairs for each field. This syntax is called a struct literal. The order of fields in the literal doesn’t need to match the definition.
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let origin = Point { x: 0.0, y: 0.0 };
All fields must be specified during instantiation unless default values or the struct update syntax are involved (covered later).
9.2.3 Accessing Fields
Access struct fields using dot notation (.
), similar to C.
println!("User email: {}", user1.email); // Accesses the email field
println!("Origin x: {}", origin.x); // Accesses the x field
Field access is generally very efficient, comparable to C struct member access (see Section 9.11 on Memory Layout).
9.2.4 Mutability
Struct instances are immutable by default. To modify fields, the entire instance binding must be declared mutable using mut
. Rust does not allow marking individual fields as mutable within an immutable struct instance.
struct Point { x: f64, y: f64 } fn main() { let mut p = Point { x: 1.0, y: 2.0 }; p.x = 1.5; // Allowed because `p` is mutable println!("New x: {}", p.x); let p2 = Point { x: 0.0, y: 0.0 }; // p2.x = 0.5; // Error! Cannot assign to field of immutable binding `p2` }
If fine-grained mutability is needed, consider using multiple structs or exploring Rust’s interior mutability patterns (covered in a later chapter).
9.2.5 Destructuring Structs with let
Bindings
Pattern matching can be used with let
to destructure a struct instance, binding its fields to new variables. This can also move fields out of the struct if the field type isn’t Copy
.
#[derive(Debug)] // Added for printing the remaining struct struct Person { name: String, // Not Copy age: u8, // Copy } fn main() { let person = Person { name: String::from("Alice"), age: 30, }; // Destructure `person`, binding fields to variables with the same names. // `age` is copied, `name` is moved. let Person { name, age } = person; println!("Name: {}, Age: {}", name, age); // Name: Alice, Age: 30 // `person` cannot be used fully here because `name` was moved out. // Accessing `person.age` would still be okay (as u8 is Copy), // but accessing `person.name` or `person` as a whole is not. // println!("Original person: {:?}", person); // Error: use of moved value: `person` println!("Original age: {}", person.age); // This specific line compiles // Renaming during destructuring let person2 = Person { name: String::from("Bob"), age: 25 }; let Person { name: n, age: a } = person2; println!("n = {}, a = {}", n, a); // n = Bob, a = 25 }
Destructuring provides a concise way to extract values, but be mindful of ownership: moving a field out makes the original struct partially (or fully, if all fields are moved) inaccessible.
9.2.6 Destructuring in Function Parameters
Structs can also be destructured directly in function parameters, providing immediate access to fields within the function body. Ownership rules apply similarly: if the struct itself is passed by value and fields are destructured, non-Copy
fields are moved from the original struct passed by the caller.
struct Point { x: i32, y: i32, } // Destructure the Point directly in the function signature (takes ownership) fn print_coordinates(Point { x, y }: Point) { println!("Coordinates: ({}, {})", x, y); } // Destructure a reference to a Point (borrows) fn print_coordinates_ref(&Point { x, y }: &Point) { println!("Ref Coordinates: ({}, {})", x, y); } fn main() { let p = Point { x: 10, y: 20 }; // `p` is moved into the function because Point is not Copy by default. // If Point derived Copy, `p` would be copied instead. print_coordinates(p); let p2 = Point { x: 30, y: 40 }; // `p2` is borrowed immutably. Destructuring works on the reference. print_coordinates_ref(&p2); println!("p2.x after ref call: {}", p2.x); // p2 is still valid }
Destructuring in parameters enhances clarity by avoiding repetitive point.x
, point.y
access.