5/5
## Understanding Structs in Rust Structs in Rust are custom data types that empower you to bundle related values, potentially of different types, into a single, meaningful unit. They are fundamental for organizing data in your Rust programs. For instance, you could use a struct to represent the x and y coordinates of a point on a 2D plane. ## Types of Structs in Rust Rust offers several variations of structs, each suited for different scenarios. ### Named-Field Structs This is the most prevalent type of struct. Each piece of data within the struct, known as a field, is assigned a name and a specific data type. **Definition:** To define a named-field struct, you use the `struct` keyword, followed by the struct's name, and then curly braces `{}` enclosing the field definitions. Each field definition consists of a `name: type` pair. **Code Example:** ```rust struct Point { x: i32, y: i32, } ``` **Explanation:** The code above defines a struct named `Point`. It has two fields: `x` of type `i32` (a 32-bit signed integer) and `y`, also of type `i32`. ### Tuple Structs Tuple structs resemble tuples. They possess a name, but their fields are anonymous and are accessed by their numerical index (starting from 0). These are useful when you want to give a tuple a distinct type name, especially if naming individual fields would be overly verbose or redundant. **Definition:** Tuple structs are defined using the `struct` keyword, the struct name, and then parentheses `()` containing the types of the fields. **Code Example:** ```rust struct Point3D(i32, i32, i32); ``` **Explanation:** This defines a tuple struct named `Point3D`. It contains three `i32` values, which could represent the x, y, and z coordinates for a point in 3D space. ### Unit-like Structs (Empty Structs) Unit-like structs, or empty structs, are structs that have no fields at all. They are primarily useful when you need to implement a trait (a way to define shared behavior) on a type, but the type itself doesn't need to store any data. **Definition:** A unit-like struct is defined with the `struct` keyword and its name, followed by a semicolon. **Code Example:** ```rust struct Empty; ``` **Explanation:** The `Empty` struct is defined without any fields. ### Nested Structs Structs can be composed within other structs, meaning a struct can have fields whose types are other structs. This allows for creating more complex data structures. **Definition:** You define a struct that includes another struct type as one of its fields. **Code Example:** ```rust // Assuming the Point struct is already defined: // struct Point { // x: i32, // y: i32, // } struct Circle { radius: u32, center: Point, // Nested struct } ``` **Explanation:** A struct named `Circle` is defined. It has a `radius` field of type `u32` (an unsigned 32-bit integer, suitable for values that cannot be negative, like a radius) and a `center` field, which is an instance of the previously defined `Point` struct. ## Working with Struct Instances Once a struct is defined, you can create instances of it and interact with its data. ### Initializing Named-Field Structs To create an instance (a concrete value) of a named-field struct, you specify the struct name followed by curly braces containing `key: value` pairs for each field. **Code Example:** ```rust // Assuming Point struct is defined: // struct Point { // x: i32, // y: i32, // } fn main() { let p = Point { x: 1, y: 1 }; } ``` **Explanation:** An instance of the `Point` struct, named `p`, is created. Its `x` field is initialized to `1`, and its `y` field is initialized to `1`. ### Printing Structs: The `Debug` Trait Attempting to print a struct instance directly using `println!("{}", instance_name);` will lead to a compile-time error. Rust's default formatter doesn't know how to display custom struct types. **Error Indication:** The compiler will typically state that the trait `std::fmt::Display` is not implemented for your struct type. **Solution:** To enable printing structs for debugging, you can automatically derive the `Debug` trait. This is done by adding the `#[derive(Debug)]` attribute directly above the struct definition. **Code Modification (Struct Definition):** ```rust #[derive(Debug)] // Add this line struct Point { x: i32, y: i32, } #[derive(Debug)] // Also needed for nested structs if they are to be printed struct Circle { radius: u32, center: Point, } ``` **Code Modification (Printing):** Use the `{:?}` specifier within the `println!` macro for debug printing. ```rust // In main(), assuming p is an instance of Point // let p = Point { x: 1, y: 1 }; // println!("{:?}", p); // Output: Point { x: 1, y: 1 } ``` For a more readable, multi-line ("pretty-printed") output, use the `{:#?}` specifier. ```rust // Assuming circle is initialized: // let circle = Circle { radius: 1, center: Point { x: 0, y: 0 } }; // println!("{:#?}", circle); ``` **Pretty Print Output Example:** ``` Circle { radius: 1, center: Point { x: 0, y: 0, }, } ``` ### Accessing Struct Fields You can access the data stored in a struct's fields using dot notation. **Named-Field Structs:** Use `instance_name.field_name`. ```rust // #[derive(Debug)] // struct Point { x: i32, y: i32 } // let p = Point { x: 1, y: 1 }; // println!("x: {}, y: {}", p.x, p.y); // Output: x: 1, y: 1 ``` **Tuple Structs:** Use `instance_name.index` (0-based indexing). ```rust // #[derive(Debug)] // struct Point3D(i32, i32, i32); // let p3d = Point3D(-1, 0, -1); // println!("point 3D: ({}, {}, {})", p3d.0, p3d.1, p3d.2); // Output: point 3D: (-1, 0, -1) ``` Note: To print `p3d` directly with `{:?}`, `Point3D` would also need `#[derive(Debug)]`. ### Initializing Unit-like Structs Since unit-like structs have no fields, you initialize them simply by using the struct name. **Code Example:** ```rust // #[derive(Debug)] // Needed for printing // struct Empty; // let empty_instance = Empty; // To print, Empty would also need #[derive(Debug)] // println!("{:?}", empty_instance); ``` ### Initializing Nested Structs When initializing a struct that contains another struct as a field (a nested struct), you initialize the inner struct as part of the outer struct's field initialization. **Code Example:** ```rust // Ensure Point and Circle have #[derive(Debug)] // #[derive(Debug)] // struct Point { x: i32, y: i32 } // #[derive(Debug)] // struct Circle { radius: u32, center: Point } // In main(): // let circle_instance = Circle { // radius: 1, // center: Point { x: 0, y: 0 }, // }; // println!("{:#?}", circle_instance); ``` ## Common Struct Operations Rust provides convenient syntax for common operations involving structs. ### Field Init Shorthand If the variables you are using to initialize a struct's fields have the exact same names as the struct fields themselves, Rust offers a shorthand syntax. You only need to write the name once. **Code Example:** ```rust // #[derive(Debug)] // struct Point { x: i32, y: i32 } // In main(): // let x_coord: i32 = 1; // let y_coord: i32 = 1; // Long form: // let p_long = Point { x: x_coord, y: y_coord }; // Shorthand (if variable names match field names): // let x: i32 = 1; // Variable name 'x' matches field name 'x' // let y: i32 = 1; // Variable name 'y' matches field name 'y' // let p_short = Point { x, y }; // x field gets value of variable x, y field from variable y // println!("{:?}", p_short); ``` In this shorthand `Point { x, y }`, `x` implies `x: x` and `y` implies `y: y`. ### Struct Update Syntax: Creating New Instances from Old It's common to need a new struct instance that reuses most of an existing instance's values but changes a few. The struct update syntax `..` allows you to achieve this concisely. It specifies that any remaining fields not explicitly set should take their values from another instance. **Code Example:** ```rust // #[derive(Debug)] // struct Point { x: i32, y: i32 } // In main(): // let p0 = Point { x: 1, y: 2 }; // Create p1, change x to a new value (e.g., 5), but keep y from p0 // Long form: // let p1_long = Point { x: 5, y: p0.y }; // Using struct update syntax: // Let's create p1 with x = 5, and y copied from p0.y // let p1 = Point { x: 5, ..p0 }; // println!("p0: {:?}", p0); // println!("p1 (updated from p0): {:?}", p1); // Output for p1: Point { x: 5, y: 2 } ``` **Important Note on Ownership and `Copy` Trait:** The `..` syntax moves data if the types of the fields involved do not implement the `Copy` trait. For simple types like `i32` (used in `Point`), which do implement `Copy`, the original instance (`p0` in the example) remains usable after creating `p1`. However, if `Point` contained a field of a type like `String` (which does not implement `Copy`), using `p0` in the struct update syntax for `p1` would move the `String` data. Consequently, `p0` (or at least its `String` field) would no longer be usable unless the `Point` struct itself explicitly implemented the `Copy` trait (which is not possible by default if it contains non-`Copy` types like `String`). ### Modifying Struct Fields To change the value of a field in a struct instance after it has been created, the instance must be declared as mutable using the `mut` keyword. You can then use dot notation to access the field and assign a new value. **Code Example:** ```rust // #[derive(Debug)] // struct Point { x: i32, y: i32 } // In main(): // let mut p_update = Point { x: 1, y: 1 }; // println!("Initial p_update: {:?}", p_update); // p_update.x += 1; // Increment x // p_update.y = 99; // Set y to a new value // println!("Updated p_update: {:?}", p_update); // Output: Updated p_update: Point { x: 2, y: 99 } ``` ## Key Concepts Recap * **Structs:** Blueprints for creating custom data types by grouping related data into a named structure. * **Fields:** The individual pieces of data within a struct, each with a name (in named-field structs) and a type. * **Instances:** Concrete values created from a struct blueprint. * **`#[derive(Debug)]`:** An attribute that automatically implements the `Debug` trait for a struct, enabling its instances to be printed for debugging purposes using `{:?}` (standard debug format) or `{:#?}` (pretty-printed debug format). * **Mutability (`mut`):** A keyword required to declare a struct instance as mutable, allowing its field values to be changed after initialization. * **Field Init Shorthand:** A concise syntax for initializing struct fields when the local variable names used for initialization match the struct's field names. * **Struct Update Syntax (`..`):** A convenient way to create a new struct instance by copying values from an existing instance for some fields while explicitly setting others. Be mindful of data-moving behavior for fields with non-`Copy` types.
Structs in Rust are custom data types that empower you to bundle related values, potentially of different types, into a single, meaningful unit. They are fundamental for organizing data in your Rust programs. For instance, you could use a struct to represent the x and y coordinates of a point on a 2D plane.
Rust offers several variations of structs, each suited for different scenarios.
This is the most prevalent type of struct. Each piece of data within the struct, known as a field, is assigned a name and a specific data type.
Definition:
To define a named-field struct, you use the struct
keyword, followed by the struct's name, and then curly braces {}
enclosing the field definitions. Each field definition consists of a name: type
pair.
Code Example:
Explanation:
The code above defines a struct named Point
. It has two fields: x
of type i32
(a 32-bit signed integer) and y
, also of type i32
.
Tuple structs resemble tuples. They possess a name, but their fields are anonymous and are accessed by their numerical index (starting from 0). These are useful when you want to give a tuple a distinct type name, especially if naming individual fields would be overly verbose or redundant.
Definition:
Tuple structs are defined using the struct
keyword, the struct name, and then parentheses ()
containing the types of the fields.
Code Example:
Explanation:
This defines a tuple struct named Point3D
. It contains three i32
values, which could represent the x, y, and z coordinates for a point in 3D space.
Unit-like structs, or empty structs, are structs that have no fields at all. They are primarily useful when you need to implement a trait (a way to define shared behavior) on a type, but the type itself doesn't need to store any data.
Definition:
A unit-like struct is defined with the struct
keyword and its name, followed by a semicolon.
Code Example:
Explanation:
The Empty
struct is defined without any fields.
Structs can be composed within other structs, meaning a struct can have fields whose types are other structs. This allows for creating more complex data structures.
Definition:
You define a struct that includes another struct type as one of its fields.
Code Example:
Explanation:
A struct named Circle
is defined. It has a radius
field of type u32
(an unsigned 32-bit integer, suitable for values that cannot be negative, like a radius) and a center
field, which is an instance of the previously defined Point
struct.
Once a struct is defined, you can create instances of it and interact with its data.
To create an instance (a concrete value) of a named-field struct, you specify the struct name followed by curly braces containing key: value
pairs for each field.
Code Example:
Explanation:
An instance of the Point
struct, named p
, is created. Its x
field is initialized to 1
, and its y
field is initialized to 1
.
Debug
TraitAttempting to print a struct instance directly using println!("{}", instance_name);
will lead to a compile-time error. Rust's default formatter doesn't know how to display custom struct types.
Error Indication:
The compiler will typically state that the trait std::fmt::Display
is not implemented for your struct type.
Solution:
To enable printing structs for debugging, you can automatically derive the Debug
trait. This is done by adding the #[derive(Debug)]
attribute directly above the struct definition.
Code Modification (Struct Definition):
Code Modification (Printing):
Use the {:?}
specifier within the println!
macro for debug printing.
For a more readable, multi-line ("pretty-printed") output, use the {:#?}
specifier.
Pretty Print Output Example:
You can access the data stored in a struct's fields using dot notation.
Named-Field Structs:
Use instance_name.field_name
.
Tuple Structs:
Use instance_name.index
(0-based indexing).
Note: To print p3d
directly with {:?}
, Point3D
would also need #[derive(Debug)]
.
Since unit-like structs have no fields, you initialize them simply by using the struct name.
Code Example:
When initializing a struct that contains another struct as a field (a nested struct), you initialize the inner struct as part of the outer struct's field initialization.
Code Example:
Rust provides convenient syntax for common operations involving structs.
If the variables you are using to initialize a struct's fields have the exact same names as the struct fields themselves, Rust offers a shorthand syntax. You only need to write the name once.
Code Example:
In this shorthand Point { x, y }
, x
implies x: x
and y
implies y: y
.
It's common to need a new struct instance that reuses most of an existing instance's values but changes a few. The struct update syntax ..
allows you to achieve this concisely. It specifies that any remaining fields not explicitly set should take their values from another instance.
Code Example:
Important Note on Ownership and Copy
Trait:
The ..
syntax moves data if the types of the fields involved do not implement the Copy
trait. For simple types like i32
(used in Point
), which do implement Copy
, the original instance (p0
in the example) remains usable after creating p1
. However, if Point
contained a field of a type like String
(which does not implement Copy
), using p0
in the struct update syntax for p1
would move the String
data. Consequently, p0
(or at least its String
field) would no longer be usable unless the Point
struct itself explicitly implemented the Copy
trait (which is not possible by default if it contains non-Copy
types like String
).
To change the value of a field in a struct instance after it has been created, the instance must be declared as mutable using the mut
keyword. You can then use dot notation to access the field and assign a new value.
Code Example:
Structs: Blueprints for creating custom data types by grouping related data into a named structure.
Fields: The individual pieces of data within a struct, each with a name (in named-field structs) and a type.
Instances: Concrete values created from a struct blueprint.
#[derive(Debug)]
: An attribute that automatically implements the Debug
trait for a struct, enabling its instances to be printed for debugging purposes using {:?}
(standard debug format) or {:#?}
(pretty-printed debug format).
Mutability (mut
): A keyword required to declare a struct instance as mutable, allowing its field values to be changed after initialization.
Field Init Shorthand: A concise syntax for initializing struct fields when the local variable names used for initialization match the struct's field names.
Struct Update Syntax (..
): A convenient way to create a new struct instance by copying values from an existing instance for some fields while explicitly setting others. Be mindful of data-moving behavior for fields with non-Copy
types.
A foundational guide to Understanding Structs in Rust - Define custom data types using named-field, tuple, unit-like, and nested structs, essential building blocks in Rust. Master instance creation, modification, `Debug` printing, and concise shorthands for initialization and updates.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on July 11, 2025
Rust Developer
Rust Programming BasicsDuration: 6min
Duration: 18min
Duration: 47min
Duration: 15min
Duration: 19min
Duration: 8min
Duration: 12min
Duration: 46min
Duration: 14min
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on July 11, 2025