If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
5.1.1 What Is a Struct
The meaning of struct is “structure”. It is a custom data type that allows programs to name and bundle related values into meaningful combinations. It is similar to a “class” or “structure” in other programming languages, but it only provides data storage and does not include methods.
People who have studied C/C++ may already be very familiar with the struct keyword, but there are differences:
C:
structis a simple aggregate type used to organize data. It can contain only data and no methods.C++:
structis very similar toclass. It can contain data and methods, and the only syntax difference is that the default access level instructispublic, while inclassit isprivate.Rust:
structis used only to define data structures and does not include methods. Methods must be defined for the struct through animplblock. Rust provides stricter ownership, lifetime, and memory management mechanisms.
5.1.2 Defining a Struct
- Use the
structkeyword to name the entire struct using CamelCase. - Inside curly braces, define the name and type of every field.
Example:
Create a struct customized to store various data for CS professional players on HLTV (additional info: CS professional player data generally consists of Rating, DPR, KAST, Impact, ADR, and KPR).
struct Stats{
rating: f32,
dpr: f32,
kast: f32,
impact: f32,
adr: f32,
kpr: f32,
}
5.1.3 Instantiating a Struct
To use a struct, you need to create an instance of it:
- Assign a concrete value to each field; you cannot omit field values.
- There is no need to specify them in the order in which they were declared.
Using donk as an example, create his database:
fn main() {
let donk = Stats {
rating: 1.27,
impact: 1.4,
dpr: 0.67,
adr: 88.8,
kast: 74.1,
kpr: 0.85,
};
}
5.1.4 Accessing the Value of a Field in a Struct
You can use dot notation to access a field’s value in a struct:
fn main() {
let mut donk = Stats {
rating: 1.27,
impact: 1.4,
dpr: 0.67,
adr: 88.8,
kast: 74.1,
kpr: 0.85,
};
donk.rating = 2.59;
}
If you want to change a struct’s values, remember to use the mutable variable keyword mut when instantiating it.
In a struct, the smallest unit of mutability is the entire instance, so you cannot control the mutability of a single field on its own. Once a struct instance is declared mutable, all fields in that instance are mutable.
5.1.5 Using a Struct as a Function Return Value
The last expression in a function is its return value, so if you use a struct as a return value, you only need to make sure that constructing the struct is the last expression in the function (without a semicolon):
fn change_stats(rating: f32, impact:f32, dpr:f32, adr:f32, kast:f32, kpr:f32) -> Stats{
Stats {
rating: rating,
impact: impact,
dpr: dpr,
adr: adr,
kast: kast,
kpr: kpr,
}
}
5.1.6 Field Init Shorthand
Rust, like JS and C#, allows field initialization to be shortened in some cases.
When a field name and the corresponding variable name for the field value are the same, you can use shorthand. For example, in the previous code snippet, all field names are the same as the variable names for their values, so it can be shortened to:
fn change_stats(rating: f32, impact:f32, dpr:f32, adr:f32, kast:f32, kpr:f32) -> Stats{
Stats {
rating,
impact,
dpr,
adr,
kast,
kpr,
}
}
Of course, this is not limited to cases where everything matches. As long as one field meets the shorthand condition, you can use the shorthand there and keep the normal syntax for the others.
5.1.7 Struct Update Syntax
When you create a new instance based on an existing struct instance, and the new instance has fields that are the same as the old one, you can use update syntax.
For example, if I want to create data for sh1ro, where his rating is 1.25, his impact is 1.2, and the rest are the same as donk’s, this is the basic form:
fn main() {
let donk = Stats {
rating: 1.27,
impact: 1.4,
dpr: 0.67,
adr: 88.8,
kast: 74.1,
kpr: 0.85,
};
let sh1ro = Stats {
rating: 1.25,
impact: 1.2,
dpr: donk.dpr,
adr: donk.adr,
kast: donk.kast,
kpr: donk.kpr,
};
}
This is a bit cumbersome, so Rust provides this syntactic sugar:
fn main() {
let donk = Stats {
rating: 1.27,
impact: 1.4,
dpr: 0.67,
adr: 88.8,
kast: 74.1,
kpr: 0.85,
};
let sh1ro = Stats {
rating: 1.25,
impact: 1.2,
..donk
};
}
You only need to write the parts that changed. For the rest, just write .. followed by the name of the other struct instance, which means that the values of the remaining unassigned fields are the same as the corresponding fields in the other instance.
5.1.8 Tuple Structs
A tuple struct is a type of struct that is similar to a tuple. The whole tuple struct has a name, but the elements inside it do not. It is useful when you want to name an entire tuple, make it distinct from other tuples, and do not need to name each element.
To define a tuple struct, use the struct keyword followed by the name and the types of the elements inside it.
Example:
struct Color(u8, u8, u8);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Some people jokingly say that tuple structs have no equivalent in traditional programming languages and come from the noble lineage of Haskell. This is because in many traditional object-oriented languages, such as Java and C++, structs or classes are named and have named fields, while tuples are anonymous and based only on order. There is no intermediate form that combines the strengths of both. Rust’s tuple struct concept is directly related to Haskell’s Newtype Pattern. In Haskell, you can define a similar pattern with newtype.
It is worth noting that even if two tuple structs have the same number of elements and the corresponding element types are identical, they should not be considered the same type, because they are different structs.
5.1.9 Unit-Like Structs
They behave similarly to the unit type (). They are used when you need a type marker or want to implement a trait on some type (which you can think of as an interface) without storing any data in the type itself. This is similar to interface{} in Go.
struct ReadOnly;
struct WriteOnly;
fn process_data<T>(_mode: T) {
// Used only as a type marker
}
fn main() {
process_data(ReadOnly);
process_data(WriteOnly);
}
This example implements type markers.
5.1.10 Ownership of Struct Data
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
In this example, both username and email use the String type instead of &str, because String is an owned type and owns all of its data. In this case, as long as the instance is valid, the field data inside it is also definitely valid.
Reference types such as &str can also be stored in a struct, but that requires lifetimes (which we will cover later). Simply put, lifetimes ensure that as long as the struct instance is valid, the references inside it are also valid. If a struct stores references without using lifetimes, it will produce an error (missing lifetime specifier).