Structs
Define and Create 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 struct
s 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 thestruct
's data.self
takes ownership of thestruct
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 T
s; 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.