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 use snake_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.