Structs

Define and Create Structs

Rust by example - Structs

Structs are custom data types that allow you to group related data together. The following example demonstrates how to define a struct and create a instance of it:

/// We first define the struct's fields (which can be of any type).
/// The `User` struct has four fields: `active`, `username`, `email`, and
/// `sign_in_count`.
#[derive(Debug)]
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
// The `#[derive(Debug)]` attribute is not required; it enables the use of
// `println!` below.

/// The `main` function creates an instance of the `User` struct.
/// It then prints the struct to the console.
fn main() {
    // Note that there is no `new` keyword or similar.
    // The order of fields doesn't need to match the definition.
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
    println!("{user1:?}");

    // Access fields with the `.` operator:
    let _ = user1.active;
}

Struct fields follow the general rule of everything being private by default, unless annotated with pub⮳. See the Visibility chapter.

Struct fields may be a primitive type, a tuple, an array, an enum, another struct (for nested structs), a reference...

#[derive(Debug)]
enum Status {
    Active,
    Inactive,
}

#[derive(Debug)]
struct InnerStruct(i32);

#[derive(Debug)]
struct ComplexStruct<'a> {
    primitive: u32,            // Primitive type.
    tuple_field: (i32, f64),   // Tuple type.
    array_field: [u8; 3],      // Array type.
    enum_field: Status,        // Enum type.
    struct_field: InnerStruct, // Another struct, here a tuple struct.
    reference_field: &'a str,  // Reference type.
}

fn main() {
    let complex = ComplexStruct {
        primitive: 10,
        tuple_field: (5, 3.1),
        array_field: [1, 2, 3],
        enum_field: Status::Active,
        struct_field: InnerStruct(42),
        reference_field: "Hello, world!",
    };

    println!("{complex:?}");
}

Declare Tuple Structs and Unit-like Structs

You can also define tuple struct (no field names) and "Unit-like" structs (without fields). Unit-like structs are so named, because they behave similarly to ().

/// A tuple struct.
///
/// Note the ( ) and the lack of field names.
/// It behaves like a named tuple.
#[derive(Debug)]
struct Color(pub i32, pub i32, pub i32);
// We choose here to make all fields public.

/// A unit-like struct.
///
/// Unit-like structs are useful when you need to implement a trait on
/// something, but don't have any data that you want to store in the type
/// itself.
#[derive(Debug)]
struct UnitLike; // Note that there are no fields.

fn main() {
    // Initialize the tuple struct:
    let black = Color(0, 0, 0);
    // Access the inner field with .0, .1, etc.:
    let _red_value = black.0;
    println!("{black:?}");

    let s = UnitLike;
    println!("{s:?}");
}

Design Structs for Ownership

As shown in an example above, it is possible to declare structs, which fields are references. A lifetime parameter (e.g. <'a>) must be specified. See the Lifetimes chapter for more details.

However, you will typically design structs for ownership, so that the data within the struct is valid for as long as the struct itself is valid.

That means that struct fields should use T instead of &T or &mut T, String instead of &str, PathBuf instead of &Path, OsString instead of &OsStr, etc.

Note that self-referential structs, that is structs that hold a reference to their own fields, can be tricky due to Rust's ownership and borrowing rules. You can easily run into issues when moved, as the reference might become invalid. There is also no way to tie the lifetime of a reference to the lifetime of the struct that contains it.

Create an Instance of Struct from Another with the Struct Update Syntax

You may update a struct by using the ..previous_instance to fill in the rest.

/// Represents a user with their active status and username.
#[derive(Debug)]
struct User {
    active: bool,
    username: String,
}

fn main() {
    // Create a first instance of the struct:
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
    };

    // Create a second instance by updating it:
    let user2 = User {
        // Modify the `username` field only.
        username: String::from("Bob"),

        // `..` is used to "fill in the rest".
        // The remaining fields not explicitly set will have the same value
        // as the fields in the given instance.
        ..user1
    };
    println!("{user2:?}");
}

Implement and Use the Methods and Associated Functions of a Struct

Methods and associated functions are defined within an impl (implementation) block.

Methods are functions that are associated with a specific instance of a struct. Their first parameter (self) refers to the struct type, or a type alias or associated type thereof.

The first parameter of a method is usually self, &self, or &mut self, which is shorthand syntax for self: Self, self: &Self, and self: &mut Self, respectively; Self itself is an alias for the implementing type.

  • &self borrows the struct instance immutably. Used for methods that only read data.
  • &mut self borrows the struct instance mutably. Used for methods that need to modify the struct's data.
  • self takes ownership of the struct instance. This is less common, unless the method is intended to consume the instance.

The first parameter of a method can also be self: Box<Self>, self: Arc<Self>, self: Rc<Self>, self: Pin<&Self>, or a combination of these types.

Associated functions, in contrast, do not take self as a parameter. They are often constructor-like, returning Self (like String::new, String::from):

/// A struct representing a rectangle with a width and height.
struct Rectangle {
    width: u32,
    height: u32,
}

/// Implementation block (multiple blocks are allowed for a given struct).
impl Rectangle {
    /// Method: note the paramater that reference `Self`, an alias
    /// for the type that the `impl` block is for, in this case `Rectangle`.
    fn area(&self) -> u32 {
        // `&self` is short for `self: &Self`, that a function paramater that is
        // an immutable reference to `Self`.
        self.width * self.height
    }

    /// Associated Function.
    /// Note that there are NO `self`, `&self`, or `&mut self`.
    /// This is often used to define a "constructor": `SomeType::new(...)`.
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    // Call the associated function.
    // Note the syntax: `<Type>::<function>`.
    let sq: Rectangle = Rectangle::square(5);

    // Call the method. Note the dot operator.
    println!("Area: {}", sq.area());
    // This is equivalent to `Rectangle::area(&sq)`.
}

Each struct is allowed to have multiple impl blocks. Multiple impl blocks are useful when implementing generic types and traits - see below; also refer to the Generics chapters.

Note that Rust automatic references and dereferences when calling methods: When you call a method with object.something(), Rust automatically adds in &, &mut, or *, so object matches the signature of the method. In other words, the following are the same:

p1.distance(&p2);
(&p1).distance(&p2);

Initialize Structs with a Constructor-like (Associated) Function

It is common to define a function or an associated function to initialize the struct:

/// Represents a user with their active status, username, and email.
#[derive(Debug)]
struct User {
    active: bool,
    username: String,
    email: String,
}

// It is common to define a function or an associated function (see below) that
// initializes the struct.
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        // If function parameters or variables have the same name as the struct
        // fields, you can use a shorthand.
        username, // Instead of writing `username: username`.
        email,    // Same.
    }
}

// Implementation block for the struct.
impl User {
    // Associated function.
    // By convention, the constructor is called "new", but that is not required.
    fn new(email: String, username: String) -> User {
        User {
            active: true,
            username,
            email,
        }
    }
}

fn main() {
    // We create an instance of the struct by calling a function:
    let user1: User = build_user("user@example.com".into(), "user".to_string());

    println!("user1: {user1:?}");

    // Or with an associated function. Note the path syntax
    // `<Type>::<function>`.
    let user2: User = User::new("user@example.com".into(), "user".to_string());

    println!("user2: {user2:?}");
}

Define an Associated Constant for a Struct

You can also define associated constants in the implementation of a struct:

struct MyToken;

impl MyToken {
    // Inherent implementations can contain associated constants,
    // in addition to associated functions or methods.
    const ID: i32 = 1;
}

fn main() {
    println!("ID: {}", MyToken::ID);
}

Implement a Trait for a Struct

Rust achieves polymorphism through traits. A trait defines a set of functions and methods (and associated types and constants) that a type can implement. When a struct implements a trait, it promises to provide concrete implementations for the (non-default) functions, methods, types, constants defined by that trait.

The syntax for trait implementations is impl TraitName for StructName { ... }, where impl is the keyword to start an implementation block, TraitName is the name of the trait you want to implement, and StructName is the specific struct for which you are providing the implementation.

The items you define within the impl block must match the signatures (for functions: name, parameters, return type) defined in the trait.

Like regular struct methods (see above), trait methods use self, &self, or &mut self as their first parameter to specify how they interact with an instance of the struct.

Trait implementations can also include the definition of associated types and constants. See the Traits chapter for more details.

The following example demonstrates various implementations:

// First, define a trait (if it's not a pre-existing one):
trait SomeBehavior {
    // Method declaration (signature only, no body):
    fn do_something(&self) -> String;

    // Method with default implementation:
    fn another_action(&self, value: i32) {
        println!("The default implementation received the value: {value}");
    }

    // Associated function. In this case, it references the associated type
    // defined below. Note the path syntax `Self::<AssociatedType>`.
    fn assoc_func() -> Self::Output;

    // Associated type:
    type Output;

    // Associated constants with and without default:
    const CONSTANT: i32 = 42;
    const ANOTHER_CONST: &str;
}

// Define your struct:
struct MyStruct {
    data: i32,
    name: String,
}

// Implement the trait for your struct:
impl SomeBehavior for MyStruct {
    // Define the associated type:
    type Output = String;

    // Define the const that did not have a default:
    const ANOTHER_CONST: &str = "Another constant value";
    // Override the const that did have a default:
    const CONSTANT: i32 = 43;

    // Concrete implementation of a trait method for this struct:
    fn do_something(&self) -> String {
        format!("{} is doing something with data: {}", self.name, self.data)
    }

    // Override the default implementation of a method:
    fn another_action(&self, value: i32) {
        println!("{} received value: {}", self.name, value);
    }

    // Implement the associated function:
    fn assoc_func() -> Self::Output {
        "Associated function output".to_string()
    }
}

fn main() {
    let m = MyStruct {
        data: 42,
        name: "Alice".to_string(),
    };
    // Use the `.` operator to call a method on a type that implements it:
    println!("{}", m.do_something());
    m.another_action(10);

    println!("{}", MyStruct::assoc_func());
    println!("{}", MyStruct::CONSTANT);
    println!("{}", MyStruct::ANOTHER_CONST);
}

Implement Common Standard Library Traits

Rather then forcing you to implement very common traits repeatedly, Rust provides a derive attribute that can autoimplement them for you:

//! Implementation of Common Traits on a `struct`.

use std::fmt;
use std::hash::Hash;

/// The `derive` attribute allows the quick and implementation of very common
/// traits:
/// - The `Debug` trait is used to provide a programmer-facing, detailed, string
///   representation of a type.
/// - `Clone` allows you to create an explicit, independent copy (a "clone") of
///   a value.
/// - `Copy` is a special marker trait indicating that a type's values can be
///   duplicated by a simple bitwise copy (like copying bytes in memory). Types
///   that are Copy are implicitly copied when they are assigned, passed to
///   functions, or returned from functions, rather than being moved (which
///   would invalidate the original). All fields (i32) are `Copy`, so Point2D
///   can be `Copy`.
/// - `PartialEq` (and `Eq`) allow instances of a type to be compared for
///   equality (==) and inequality (!=).
/// - `PartialOrd` (and `Ord`) allows instances of a type to be ordered, meaning
///   you can compare them using <, >, <=, and >=.
/// - `Hash` allows instances of a type to be "hashed," which means producing a
///   single u64 value that represents the instance.
/// - `Default` allows for the creation of a "default" or "initial" instance of
///   a type, often representing a zeroed, empty, or baseline state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Point2D {
    x: i32,
    y: i32,
}

// These traits can of course be manually implemented:
// impl fmt::Debug for Point2D {
//     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//         f.debug_struct("Point")
//             .field("x", &self.x)
//             .field("y", &self.y)
//             .finish()
//     }
// }

// The `Display` trait allows your struct to be printed using the `{}` format
// specifier, for user-friendly output.
impl fmt::Display for Point2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Point2D: ({}, {})", self.x, self.y)
    }
}

fn main() {
    // Create an instance of the `Point2D` struct:
    let p = Point2D { x: 1, y: 1 };

    // Use the `Debug` implementation:
    println!("{p:?}");

    // Use the `Display` implementation:
    println!("{p}");

    // Clone the point.
    // Since the type is `Copy`, you can and should just assign.
    #[allow(clippy::clone_on_copy)]
    let p2 = p.clone();
    println!("{p2:?}");

    // Since `Point2D` is `Copy`, p2 remains valid after an assignment.
    let p3 = p2;
    println!("{p2:?}");
    println!("{p3:?}");

    // Equality:
    assert_eq!(p2, p3);

    // Use the `Hash` implementation:
    let mut h = std::collections::hash_map::DefaultHasher::new();
    p.hash(&mut h);
    // `Point2D` can be used as a key in a `HashMap`.

    // Use the `Default` implementation:
    let p4 = Point2D::default();
    println!("{p4:?}");
}

See the Derive chapter for more details.

Define Generic Structs

Generic structs are like templates with one or more parameters, which act as placeholders to be specified later (e.g., during variable declaration or instance creation). They are declared by adding the aforementioned parameters between < and >, after the name of the struct. Their use promote code reuse.

Generic structs can be parameterized by types and (more infrequently) lifetimes and constants.

The following example demonstrates the declaration, implementation, and instance creation of a generic struct with a type parameter T.

Note how multiple impl blocks are possible; they can implement associated functions and methods for all possible Ts; for specific types only; or conditionally, only for types that implement certain traits. See Generics for more details.

use std::fmt::Display;

/// A generic struct `Point` that can hold two values of the same type `T`:
struct Point<T> {
    x: T,
    y: T,
}

/// Generic implementation block for `Point<T>` where `T` can be any type:
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

/// Implementation block for `Point<f32>` only:
impl Point<f32> {
    /// A method specific to `Point<f32>`:
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

/// You may also use Trait Bounds to conditionally implement methods.
/// The following is only implemented for `Point<T>` when `T` implements both
/// the `Display` and `PartialOrd` traits.
impl<T: Display + PartialOrd> Point<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

fn main() {
    // Instantiate `Point` with integer coordinates (type `i32`).
    let integer_point = Point { x: 5, y: 10 };

    // Instantiate `Point` with floating-point coordinates (type `f64`).
    let float_point = Point { x: 1.0, y: 4.0 };

    // Rust can often infer the type, but you could also be explicit:
    // let integer_point: Point<i32> = Point { x: 5, y: 10 };
    // let float_point: Point<f64> = Point { x: 1.0, y: 4.0 };

    println!(
        "Integer point: x = {}, y = {}",
        integer_point.x, integer_point.y
    );
    println!("Float point: x = {}, y = {}", float_point.x, float_point.y);

    // This would cause a compile-time error because x and y must be the same
    // type `T`: let wont_compile = Point { x: 5, y: 4.0 };
}

References

Creating Structs In Rust: Builder Pattern, Fluent Interfaces, And More⮳.

Related Topics

  • Enums.
  • Lifetimes.
  • Generics.
  • Traits.
  • Rust Patterns.