Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

#![allow(unused)]
fn main() {
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.

struct Point {
    x: f64,
    y: f64,
}
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
fn main() {
    let active = true;
    let x = 0.0;
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active, // active: active,
        sign_in_count: 1,
    };

    let origin = Point { x, y: 0.0 };
}

All fields must be specified during instantiation unless default values or the struct update syntax are involved (covered later). If a local variable or parameter shares the same name as a struct field, you can use the shorthand by writing the name once instead of the full field_name: field_name form.

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). Note: In Rust, the dot syntax is always used for field access—even when working with references to structs. Unlike C, where pointers require the -> operator, Rust automatically dereferences when needed.

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 Field Access on Borrowed Structs

When working with references to struct instances—either as function parameters or local references—you can access fields according to standard borrowing rules:

  • Immutable references allow reading field values.
  • Mutable references allow both reading and modifying field values. However, you cannot move a field out of a borrowed struct. Moving would invalidate the original struct instance, which Rust disallows for borrowed data.

Here’s an example illustrating these rules:

struct T {
    a: String,
}

fn main() {
    let mut t = T { a: "X".to_owned() };
    update(&mut t);
}

fn update(x: &mut T) {
    // Read access to a field through a reference
    println!("{}", &x.a[0..1]);

    // Write access to a field through a mutable reference
    x.a.replace_range(0..1, "Y");

    println!("{}", &x.a[0..1]);

    // Attempting to move the field out of the struct would fail:
    // let moved = x.a;
    // error[E0507]: cannot move out of `x.a` which is behind a mutable reference
}

9.2.6 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.7 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.