A struct, short for structure, is a composite data type in Rust that groups multiple primitive or composite data types together under a single name, using fields. It is declared with the struct keyword, followed by the struct name, and within {} we define the fields and their data types. A common naming convention for declaring structs in Rust is PascalCase for the struct name, while field names are written in snake_case.
use std::path::PathBuf;
struct MyStruct {
field_a: i32, // integer
field_b: String, // String
field_c: (u8, u8), // tuple
field_d: Option<u32>, // enum
field_e: PathBuf, // struct
}
In the example above, we can see that structs not only hold fields with primitive data types such as integers, but also composite types like String, tuples, enums, and even other structs. Remember, a struct is just a type. To create an instance of a struct, we need to define a variable, just like with other types, and provide values for all its fields.
struct MyStruct {
field_a: i32,
field_b: String,
field_c: (u8, u8),
field_d: Option<u32>,
}
fn main() {
let ms: MyStruct = MyStruct {
field_a: 1,
field_b: "Hello, world!".to_string(),
field_c: (2, 3),
field_d: Some(4),
};
println!("ms.field_a: {}", ms.field_a);
println!("ms.field_b: {}", ms.field_b);
println!("ms.field_c: {:?}", ms.field_c);
println!("ms.field_d: {:?}", ms.field_d);
}
ms.field_a: 1
ms.field_b: Hello, world!
ms.field_c: (2, 3)
ms.field_d: Some(4)
To construct a struct, we provide the name of the struct followed by its fields and their values. The order of the fields doesn’t matter. Unlike in some languages like JavaScript, where the absence of a field is allowed and the field value can be undefined, in Rust, we must provide values for all fields. To access a field value on a newly constructed struct, such as ms in our case, we use the . operator.
fn main() {
let mut ms = MyStruct {
field_a: 1,
field_b: "Hello, world!".to_string(),
field_c: (2, 3),
field_d: Some(4),
};
println!("[before] ms.field_c: {:?}", ms.field_c);
ms.field_c.1 = 33;
println!("[after] ms.field_c: {:?}", ms.field_c);
}
$ cargo run
[before] ms.field_c: (2, 3)
[after] ms.field_c: (2, 33)
Struct fields are immutable by default. When we want to modify the value of a field inside a struct, we first need to make the variable that holds the struct, ms in our case, mutable using the mut keyword. After that, we can assign a new value to the field using the <struct>.<field> = <new_value> syntax. Since in our case, field_c holds a tuple, we can modify an individual element inside the tuple using the .<position> syntax, such as field_c.1 for the second element.
Tuple Struct
If you remember tuples, it doesn’t have a name attached to it like a struct. The closest we get to define a naming a tuple is using a type alias.
type Point = (i32, i32);
fn main() {
let p: Point = (1, 2);
}
However, Point here is not a new type. It’s merely an alias for (u32, u32). Declaring let p: Point is the same as declaring let p: (u32, u32) at compile time, since the compiler substitutes the type alias with its concrete type during compilation. Unlike a struct, a type alias can’t implement traits, and it can’t have an impl block to define methods or associated functions.
We can fix this by declaring a tuple struct, which is similar to a type alias but defines a new concrete type.
struct Point(u32, u32);
fn main() {
let mut p: Point = Point(1, 2);
println!("[before] p.1: {:?}", p.1);
p.1 = 20;
println!("[after] p.1: {:?}", p.1);
}
$ cargo run
[before] p.1: 2
[after] p.1: 20
To declare a tuple struct, we follow the same approach as with a normal struct, but instead of providing fields with names and types within {}, we only provide the types within (), similar to a tuple. Unlike a normal struct, we access elements of a tuple struct using their position, such as p.1 in the case above. Tuple structs are easier to work with when the order of the elements matters or when field names aren’t important, especially for small structs like Point in the example above, where we only have two elements.
Newtype Pattern
In the Newtype Pattern, we declare a tuple struct with just one field, normally of a primitive data type such as i32, f64, bool, etc.
struct UserId(u32);
fn main() {
let mut uid: UserId = UserId(1);
println!("[before] uid: {:?}", uid.0);
uid.0 = 100;
println!("[before] uid: {:?}", uid.0);
}
$ cargo run
[before] uid: 1
[before] uid: 100
With a Newtype, we can provide a concrete type to rather a simple value such as u32 so that we can avoid mixups with other u32 types since u32 != UserId as they are two distinct types unlike a type alias. We can also implement traits for our Newtype without affectting the underlying type. We can also attach behaviour to our Newtype without affectting the underlying type.
struct UserId(u32);
impl UserId {
fn is_valid(&self) -> bool {
if self.0 == 1 {
true
} else {
false
}
}
}
fn main() {
let uid: UserId = UserId(1);
println!("is uid valid: {:?}", uid.is_valid());
}
// is uid valid: true
Here, we have implemented an is_valid method on the UserId type without affecting the underlying u32 type. Using an impl block on u32 directly is not allowed because it comes from the std crate, and Rust’s rules prevent us from modifying types we don’t own. While we could use a trait to add this method, that would place the is_valid method on the u32 type application-wide, which is not our intent, since not all u32 values represent user IDs.
We will learn more about the
implblock and method implementation in a later lesson.
Unit-Like Structs
A unit-like struct is a normal struct without any fields.
struct Connected;
impl Connected {
fn fetch_data(&self) {
println!("Fetching data...");
}
}
fn main() {
let connected: Connected = Connected;
connected.fetch_data();
}
// Fetching data...
Here, Connected is a unit-like struct. We call it a unit-like struct because it’s similar to (), which is referred to as the unit type, as it doesn’t represent any data. We could also define an empty struct like struct Connected {}, which would then need to be constructed as ... = Connected {} instead of ... = Connected;, as we did above. There is virtually no difference between them, but the former is called a unit-like struct, while the latter is called an empty struct. Most prefer a unit-like struct over an empty struct for simplicity and clarity.
As you can see, this struct doesn’t hold any data, but it can have behavior. In the above example, the Connected unit-like struct has the fetch_data() method, which provides the behavior. When an external trait provides mainly behavior and doesn’t require holding any data, a unit-like struct is the best choice.
.. syntax
When creating multiple values of the same structs, it so happens somtimes that we might want to reuse some fields of the previously defined struct into a new struct value. Let’s me explain what I mean.
#[derive(Debug)]
struct User {
name: String,
age: u8,
member: bool,
}
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
let jane = User {
name: "Jane Doe".to_string(),
..john
};
println!("john: {:?}", john);
println!("jane: {:?}", jane);
}
Here, we have declared a struct User with a field user of type String, age of type u8, and member of type bool. Ignore #[derive(Debug)] for a moment; we will explain it in more detail later. For now, it enables printing the struct’s value with println!. We define john with values for all fields, while jane only has its own user field value and copies the rest of the field values from john using the ..john syntax. The .. syntax copies the fields not already provided from another struct into the current struct.
The
..syntax is commonly called struct update syntax because it is used to update one struct with values from another. It should be the last line in the struct value declaration.
Though we describe .. as copying the rest of the field values, we need to be cautious when using it. We’ll delve into how Rust handles data ownership in upcoming sections, but for now, it’s essential to understand that in Rust, data can only be owned by one entity at a time. In the example above, the values of the name, age, and number fields in the john struct are owned by john. When we use the ..john syntax in jane, we attempt to copy the values of the age and number fields into jane, since the name field is already defined for jane. Since the u8 and bool data types implement the Copy trait, copying is allowed, meaning the values 30 and false are simply duplicated and added to jane. Since these values were duplicated, there is no ownership issue over a single resource (30 or false). Both john and jane have their own 30 and false.
However, it’s not as simple with heap-allocated data types. For example, unlike u8 and bool, String is stored on the heap for reasons we discussed in the “Strings” lesson. Since String does not implement the Copy trait, its value cannot simply be copied and assigned to another variable or struct field in this case.
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
let jane = User { ..john };
println!("jane.name: {:?}", jane.name);
println!("john.name: {:?}", john.name); // <-- error
}
$ cargo run
cargo run
error[E0382]: borrow of moved value: `john.name`
--> src/main.rs:18:33
|
14 | let jane = User { ..john };
| --------------- value moved here
...
18 | println!("john.name: {:?}", john.name); // <-- error
| ^^^^^^^^^ value borrowed here after move
In the example above, we used all the values of the john struct inside jane by simply using the .. syntax. In this case, the age and number fields were copied. However, since String does not implement the Copy trait, its value cannot be implicitly duplicated by Rust and therefore moves from john to jane. After this move, ownership of the string "John Doe" is transferred from john to jane, and john.name no longer holds the string; it becomes invalid. This is why, if we try to access this field, we encounter the error mentioned above.
let jane = User {
name: john.name.clone(),
..john
};
If you want to get the value of john.name into jane.name, you can do so explicitly by copying the string value using the .clone() method of String. String implements the Clone trait, which provides the .clone() method to create a deep copy of the String data. This way, we avoid ownership issues over a single resource since, like u8 and bool, the String value is duplicated into jane.
Pattern matching
Destructuring can be very helpful when you want to extract only certain parts of a struct into variables more concisely. Using Rust’s pattern matching capabilities, we can access specific parts of a complex data structure, like a struct, with a simplified syntax. Let’s explore how pattern matching works with structs.
struct User {
name: String,
age: u8,
member: bool,
}
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
// name: `&String`, age: `u8`, member: `bool`
let User { ref name, age, member } = john;
println!("{}, {}, {}", name, age, member);
// ERROR: borrow of moved value: `john.name`
// println!("john.name: {}", john.name); // <-- error
}
// John Doe, 30, false
In destructuring syntax, we follow the let <pattern> = <value> format, where pattern represents the structure we want to match with value. If the pattern doesn’t match, we’ll get a compilation error, unlike a match statement where we can provide a fallback for unmatched patterns. For a typical struct, the pattern follows the form StructName { field1, field2 }, as shown above. In the example, we create fresh variables name, age, and member in the main() function, which receive values from the corresponding fields in the john struct.
In destructuring syntax, ownership of data is moved from the struct to the variables if the type does not implement the Copy trait, as seen with the name field, since String does not implement the Copy trait. Thus, when we try to access this field on the struct after destructuring, we encounter a compile-time error. If you only want to reference the data held by a struct, you can use the ref keyword in the pattern matching syntax.
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
// name: `&String`, age: `u8`, member: `bool`
let User {
ref name,
age,
member,
} = john;
println!("{}, {}, {}", name, age, member);
println!("john.name: {}", john.name);
}
$ cargo run
John Doe, 30, false
john.name: John Doe
In the example above, instead of name, we used ref name, which accesses the name field of john as a reference. Consequently, it has the type &String rather than String, as in the previous example. Since ownership of the String data behind john.name isn’t transferred, we can still use john.name after the destructuring syntax. Here, we have borrowed name immutably, meaning the data behind the reference cannot be modified. However, with pattern matching, we can also borrow a struct field mutably using the mut keyword.
fn main() {
let mut john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
// name: `&mut String`, age: `u8`, member: `bool`
let User {
ref mut name,
mut age,
member,
} = john;
name.push_str(" Jr.");
age = 31;
println!("{}, {}, {}", name, age, member);
println!("john.name: {}", john.name);
}
$ cargo run
John Doe Jr., 31, false
john.name: John Doe Jr.
In the example above, by using the ref mut syntax, we are borrowing the name field mutably, allowing us to make changes to it. With the .push_str() method, which requires a mutable reference to String, we can modify the string data by appending additional text. Since age implements the Copy trait, there is no need to borrow it with the ref keyword, as its value is simply copied into the variable.
If you are only interested in a few fields, you can use the .. syntax to ignore the other fields. This is called partial destructuring.
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: false,
};
let User { name: username, .. } = john;
println!("username: {}", username);
}
// name: John Doe
Here, the .. syntax ignores the remaining fields in the john (User) struct, indicating that the other fields do not matter. We can also use the <field>: <variable name> syntax to map a field name to a custom variable, rather than using the field name itself as the variable, as we did above.
struct User(String, u8, bool);
fn main() {
let john = User("John Doe".to_string(), 30, false);
let User(username, ..) = john;
println!("name: {}", username);
}
// name: John Doe
In the case of a tuple struct, the order of fields matters, as they are organized by their positions in the struct definition. For tuple structs, we can assign any custom variable names we want directly, without needing specific syntax. In the example above, the username variable receives the value of the first field of john, and the remaining fields are ignored using the .. syntax.
fn main() {
let john = User("John Doe".to_string(), 30, false);
let User(_, age, ..) = john;
println!("age: {}", age);
}
// age: 30
In the example above, we ignored the first field value using the _ pattern and captured the second field value in age. The remaining fields are ignored with the .. syntax. Note that we are allowed to use the .. syntax only once in the pattern matching.
fn main() {
let john = User("John Doe".to_string(), 30, false);
let User(.., member) = john;
println!("member: {}", member);
}
// member: false
In the example above, we used the .. syntax to ignore all initial fields of john and match only the last field with the member variable.
fn is_member_simple(user: User) -> bool {
user.member
}
fn is_member(User { member, .. }: User) -> bool {
member
}
Destructuring can be very powerful in various contexts. For example, when passing a value to a function, the function can use pattern matching to extract values from complex types. In the example above, both functions accept an argument of type User, but the is_member function uses destructuring to extract the member field concisely.
let users: Vec<User> = Vec::new();
for User { member, .. } in users {
println!("member: {}", member);
}
We can also use destructuring in a for loop, as shown above. Here, we have a vector of User structs, and we are looping over it using the for loop. Since we are only interested in the member field, we can use destructuring to extract just that field.
Destructuring really shines when we have nested and really complex data structure like nested structs.
struct User {
name: String,
age: u8,
member: (bool, u32), // (is_member, points)
}
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: (true, 1200),
};
let User {
member: (.., score),
..
} = john;
println!("score: {}", score);
}
We slightly modified our User struct to hold member as a tuple. In the destructuring pattern, just as we can provide a custom variable name for a field using the <field>: <new variable> syntax, we can also destructure that variable in place, as shown above. Since member is a tuple, we destructured it to extract the score value.
enum MembershipPlan {
Bronze(u32),
Gold(u32),
Platinum(u32),
}
struct User {
name: String,
age: u8,
member: MembershipPlan,
}
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: MembershipPlan::Platinum(1200),
};
let User {
member: MembershipPlan::Platinum(score),
..
} = john; // <-- error here
println!("score: {}", score);
}
$ cargo run
error[E0005]: refutable pattern in local binding
--> src/main.rs:20:9
|
20 | let User {
| _________^
21 | | member: MembershipPlan::Platinum(score),
22 | | ..
23 | | } = john;
| |_____^ patterns `User { member: MembershipPlan::Bronze(_), .. }` and `User { member: MembershipPlan::Gold(_), .. }` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
In the example above, the member field of the User struct has the type MembershipPlan, which is an enum with three variants. This means that the member field can have one of three different values. When we use a pattern with let, it needs to be irrefutable, meaning it must match all possible values. In the example, our let statement pattern only tries to match the Platinum variant of the member field, making the pattern refutable and resulting in a compile-time error.
let User {
member: MembershipPlan::Platinum(score),
..
} = john else {
panic!("Pattern did not match");
};
We can fix this error by adding an else block that diverges (i.e., returns !). With this modification, we can make the program panic if the pattern does not match. A more elegant way to handle this is by using a match statement. With match, we can provide multiple patterns and handle situations where the pattern does not match more gracefully. Since a match statement must be exhaustive, meaning it covers all possible values, we gain runtime safety by ensuring that every case is accounted for.
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
member: MembershipPlan::Platinum(1200),
};
match john {
User {
member: MembershipPlan::Platinum(score),
..
} => {
println!("John is platinum member with score: {}", score)
}
_ => {
println!("John is not a platinum member")
}
}
}
// John is platinum member with score: 1200
In the example above, the match statement includes an arm that tries to match john with the pattern User { member: MembershipPlan::Platinum(score), .. }. This pattern means that all fields except member are ignored, and member must be the Platinum variant of MembershipPlan. If the pattern matches, the associated data of the enum variant is bound to the variable score. The _ wildcard pattern matches any other possible values of john. Together, these two arms cover all possible values that john (or User) can have, making the match statement exhaustive.
Recursive Structs
In some situations, a struct field can hold value of a struct type which is quite normal. But what happens when this struct type is itself?
struct User {
name: String,
age: u8,
friend: Option<User>,
}
Here, we have a simple struct User, but the friend field is of type Option<User>. In Rust, all types must have a fixed size at compile time. As we learned in the “Strings” lesson, the String type is, behind the scenes, a struct that holds a Vec<u8>, which itself has a fixed size. The u8 type is naturally fixed at 1 byte. However, Option<User> introduces some complexity. Rust uses a fixed amount of memory to distinguish between the Some and None variants of the Option<T> enum, but the majority of the memory is used to store the T type. In our case, T is User, meaning we are once again faced with determining the size of T.
$ cargo run
error[E0072]: recursive type `User` has infinite size
--> src/main.rs:1:1
|
1 | struct User {
| ^^^^^^^^^^^
...
4 | field: User,
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
4 | field: Box<User>,
| ++++ +
When we compile the program, we encounter the above error. The Rust compiler indicates that User has an infinite size. The “recursive without indirection” part of the error message suggests that we need some form of indirection—in other words, we need to provide access to the User value indirectly. Normally, structs are stored on the stack. If a field’s value is stored on the heap, only its pointer is stored with the struct. For example, String is an abstraction over a pointer to a region of memory on the heap where the text data (the string) is stored.
Box is a struct in Rust’s standard library and is part of the prelude, which allows any type to be allocated on the heap, even integers. We will cover this data structure in detail in the “Smart Pointers” lesson, as it is one of Rust’s smart pointers. It’s called a smart pointer because Box<T> not only stores T on the heap and holds a pointer to it but also provides additional functionality and memory management features beyond a simple pointer.
Like String or Vec (Vector) which store their data on the heap and own only a pointer to it, these types have a fixed size because the size of a pointer is fixed (even though the data they reference might not be). Similarly, Box<T> is also a fixed-size type because it follows the same principle of owning a pointer to heap-allocated data. Therefore, with this change, our User struct now has a fixed size.
struct User {
name: String,
age: u8,
friend: Option<Box<User>>,
}
fn main() {
let john = User {
name: "John Doe".to_string(),
age: 30,
friend: Some(Box::new(User {
name: "Jane Doe".to_string(),
age: 25,
friend: None,
})),
};
println!("John's friend: {}", john.friend.unwrap().name);
}
$ cargo run
John's friend: Jane Doe
Let’s break down what has happened above. We declared john as usual, but the friend field is an Option, so we provided Some with its associated data, which is a Box pointer containing the User struct data. To wrap a T type with a Box pointer, we use the Box::new(T) method, which allocates memory on the heap, places T into it, and holds a pointer to it. In the example above, since our T is User, we construct another User struct value; however, this time the friend field is set to None to avoid providing another User struct value and prevent infinite recursion.
To access the value behind a pointer or reference, we use the * operator to dereference it. However, Rust provides a feature called deref coercion, which can implicitly make this conversion for us. In the example above, john.friend returns Option<Box<User>>, and calling .unwrap() extracts the Box<User> value from Some (or panics if it’s None). However, .name is then invoked on Box<User> rather than directly on User, but it still works. This is because any operation on Box<T> is automatically delegated to T under the hood. We will learn more about this and other nuances of Box in a dedicated lesson.
Important traits
Debug trait
The Debug traits provides an interface for a type to be displayed as a string. This textual output is meant for developers rather than users such that it can be printed in logs and error traces. We can derive Debug trait pretty much on any struct, as long as its fields also implement the Debug trait.
#[derive(Debug)]
struct User {
username: String,
password: String,
}
fn main() {
let john = User {
username: "John Doe".to_string(),
password: "secret".to_string(),
};
println!("john: {:?}", john);
}
// john: User { username: "John Doe", password: "secret" }
When we derive Debug on a struct, Rust automatically implements the fmt method for it. The {:?} format specifier uses the output of this method to print the struct to STDOUT. By default, Rust exposes all struct field values as strings, allowing us to see each field’s value. However, we can provide a custom implementation of the fmt method to hide, mask, or obfuscate certain fields if needed.
use std::fmt;
struct User {
username: String,
password: String,
}
impl fmt::Debug for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("User")
.field("username", &self.username)
.field("password", &"********")
.finish()
}
}
fn main() {
let john = User {
username: "John Doe".to_string(),
password: "secret".to_string(),
};
println!("john: {:?}", john);
}
// john: User { username: "John Doe", password: "********" }
Here, we provide a custom implementation of the Debug trait (which comes from the std::fmt module). The fmt function is called with a reference to the struct and a formatter f, to which we write the textual data. The debug_struct method on Formatter provides a convenient API to handle struct debug values. In this example, we supplied "********" for the password field to mask it when accessed in the debug format.
Display trait
The Display trait works similarly to Debug, but its purpose is to generate user-facing output, unlike the developer-facing output provided by the Debug trait. Unlike Debug, we can’t derive the Display trait on a type because it would be difficult for Rust to infer what users might want to see for a particular type. Therefore, we need to implement it manually, but the implementation process is not very different from that of the Debug trait.
use std::fmt;
struct User {
username: String,
password: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "User -> {}", self.username)
}
}
fn main() {
let john = User {
username: "John Doe".to_string(),
password: "secret".to_string(),
};
println!("john: {}", john);
}
// john: User -> John Doe
Like the Debug trait, we have implemented the fmt::Display trait for User, which requires us to define the fmt method. Using the write! macro, we can write string data to a writer. When we use the {} format specifier, it calls this method to produce a textual representation of User for users.
Default trait
The Default trait in Rust provides a way to specify default values for a type. Some types in Rust implement the Default trait out of the box. For example, numeric types such as u32, i32, and f64 default to 0, bool defaults to false, String defaults to String::new(), which is equivalent to an empty string, and Vec defaults to Vec::new(), which is equivalent to an empty vector.
We can also derive the Default trait on our custom types, such as structs, as long as all fields within the struct also implement it.
#[derive(Default, Debug)]
struct User {
name: String, // defaults to `""`
age: u8, // defaults to `0`
member: bool, // defaults to `false`
}
fn main() {
let john = User::default();
println!("john: {:?}", john);
}
// john: User { name: "", age: 0, member: false }
In the example above, we derived the Default trait for User, which provides the required default associated function implementation. This allows us to call User::default() to get an instance of User with all fields set to their default values.
#[derive(Debug)]
struct User {
name: String, // defaults to `""`
age: u8, // defaults to `0`
member: bool, // defaults to `false`
}
impl Default for User {
fn default() -> Self {
Self {
name: "Default Name".to_string(),
age: 21,
member: Default::default(),
}
}
}
fn main() {
let john = User::default();
println!("john: {:?}", john);
}
// john: User { name: "Default Name", age: 21, member: false }
We can also provide a custom implementation of the default() function by manually implementing the Default trait for User. All we need to do is define the default function, which returns Self, referring to User. From this function, we return an instance of User and can set any field values we want. The syntax member: Default::default() is a shorthand for member: bool::default(), which returns false.
#[derive(Debug)]
struct User {
name: String, // defaults to `""`
age: u8, // defaults to `0`
member: bool, // defaults to `false`
}
impl Default for User {
fn default() -> Self {
Self {
name: "Default Name".to_string(),
age: 21,
member: bool::default(),
}
}
}
fn main() {
let john = User {
name: "John Doe".to_string(),
..Default::default()
};
println!("john: {:?}", john);
}
// john: User { name: "John Doe", age: 21, member: false }
The Default trait is especially useful with the struct update syntax ... Since User::default() returns a struct, we can use the ..User::default() syntax to populate the remaining fields of a struct with default values. When Rust knows the type in a context—such as during variable declaration let u: User = ... or while creating a struct let u = User { ... }—we can simply use Default::default(), and Rust will infer which type’s default() method needs to be called.