Intro
Structs, short for Structures, are custom data types. They’re like mini objects that have fields and associated data, forming key-value pairs. Huh, guess I just described an object… If you’ve used C, you’re likely familiar with structs. They define the structure of an object, acting similar to a schema for that type of object.
Defining and Instantiating Structs
Similar to Tuples in Rust, the types within a struct can be different, allowing us to combine related pieces of data together, assigning values to a field (label).
Defining a struct follows the following syntax, highlighted by the struct
keyword.
Example:
struct Dev {
specialty: String,
name: String,
// in thousands
total_comp: u64,
email: String,
employed: bool,
}
Each field’s name is followed by it’s type, remember it’s best to use types who’s size is knowable at compile time. // If you need an array of strings, consider slices, vectors, or generics.
After a struct’s defined we can instantiate it the same way we do other variables using let
, each field followed by a colon to denote it’s value.
Example:
fn main(){
let dev_one = Dev{
specialty: String::from("systems programming"),
name: String::from("Foltest"),
total_comp: 124,
email: String::from("foltest@bestcomp.dev"),
employed: false
};
}
Accessing data in a struct
To access any of the fields, we use dot notation: let name = dev_one.name;
If the instance of the struct is mutable, we can also change the value this way by assigning a new one.
Mutable structs
“Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.”
Returning structs
Functions can return structs. Example:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email, // notice how the params are named the same as fields...
sign_in_count: 1,
}
}
To avoid repeating the param names over and over, especially if the struct’s got many fields… We use Field Init Shorthand.
Field Init Shorthand
This shorthand allows us to avoid repeating them.
fn build_dev(name: String, email: String, total_comp: u64) -> Dev {
Dev {
specialty: String::from(""),
name,
email,
total_comp,
employed: false,
}}
Creating new instances from others - update syntax
Sometimes you may need to make a new instance that’s similar to another with only a select few changes. The update syntax allows this.
The Update Syntax is as follows: ..other_instance
.
let new_inst = Stuu{
..other_inst
}
Example without:
fn main() {
// --snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
Example with:
let user2 = User {
email: String::from("another@example.com"), // Changing this field from the other instance
..user1
};
The update syntax must come last to specify that the remaining fields are the same.
Since this uses = to assign, the values are moved, so user1
is no longer valid.
Read more about how the update syntax works here.
Using Tuple Structs and Unnamed Fields
Tuple Structs have the semantics of a struct without naming the fields, only the types are defined. Useful for situations when you want to name the tuple and make it distinct from other tuples.
Same as before, start with the struct
keyword.
struct Tire_Pressure(f32, f32, f32, f32);
struct Coordinates(f64, f64);
fn main(){
let my_car_tires = Tire_pressure(22.1, 22.31, 21.8, 22.46);
let location = Coordinates(44.234, 183,424); // I don't know if this means anything...
}
both my_car_tires
and location
have different types, since they’re instances of different Structs.
Destructuring works on these just as it would on normal tuples: let (fl, fr, rl, rr) = my_car_tires;
Unit-Like Structs - No fields
Structs without fields can be useful when you need to implement a trait on some type but don’t have any data to store in that type. Their name is because they behave similarly to the Unit ()
.
struct AlwaysEqual; // no {} no and fields
fn main() {
let subject = AlwaysEqual;
}
You can then define behaviors and implement traits for the AlwaysEqual
struct.
More on this in Learning Rust - Ch.10.
Ownership of Struct data
You may have noticed in previous examples the string fields used actual String
types instead of &str
literals.
This means each instance owns that value, and all its data in that example, and it’ll remain valid for the lifetime of the instance, unlike a string literal which is borrowed.
Take a look at this:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "someone@example.com",
sign_in_count: 1,
};
}
If you try running this, the compiler will say that it “expected named lifetime parameter
”
Chapter 10 will discuss how to store references in structs.
Example program using Structs
Follow along here.
In this example, we want to make a program that simply calculates the area of a rectangle. //Groundbreaking I know...
But the point is to use Structs.
Tuple Structs
struct Rectangle(u32, u32);
fn main() {
let rect_one = Rectangle(1080, 1920);
println!("The area of a rectangle is {} sqr pixels.", area(&rect_one));
}
fn area(rect: &Rectangle) -> u32 {
rect.0 * rect.1
}
But what if we want more meaning? Use a normal struct.
Regular Struct
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect_one = Rectangle {
width: 1080,
height: 1920,
}; println!("The area of a rectangle is {} sqr pixels.", area(&rect_one));
}
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
Notice how the accessors for regular structs differ from tuple structs. Makes sense, tuple elements are accessed based on their index using dot notation, while regular structs use their field
Adding Functionality with traits
What if we could print an instance of a Rectangle struct? Not just print fields one by one but the whole thing.
We must implement the std::fmt::Display trait
. We know this because the compiler complains if we don’t and try to anyway.
The println!
macro does formatting using Display
trait whenever we use {}
, but only on types that implement that trait.
Why not print?
Rust prevents printing types that don’t implement the trait to avoid ambiguity.
In the compiler’s error message
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
trying println!("{:?}", rect_one)
gives the following compiler error:
--> src/main.rs:12:22
|
12 | println!("{:?}", rect_one)
| ^^^^^^^^ `Rectangle` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Rectangle` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Rectangle {
Fortunately the compiler’s very helpful and informs us to use the Debug trait and even how to add it.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect_one = Rectangle {
width: 1080,
height: 1920,
}; println!("The area of a rectangle is {} sqr pixels.", area(&rect_one));
println!("{:?}", rect_one)
}
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
// prints:
// The area of a rectangle is 2073600 sqr pixels.
// Rectangle { width: 1080, height: 1920 }
It’s not elegant but it’s okay, that’s why it’s great for debugging. Adding {:#?}
will print it even nicer:
The area of a rectangle is 2073600 sqr pixels.
Rectangle {
width: 1080,
height: 1920,
}
Debug Macro dbg!()
Unlike println!
the Debug
macro takes ownership of the value instead of a reference.
- Prints file and line number of where it’s called
- The expression’s resulting value
- Returns ownership of the value
Note
The
dbg!
macro prints to Standard Error console stream,stderr
, notstdout
likeprintln!
. This is covered in detail in “Writing Error Messages to Standard Error Instead of Standard Output” section in Chapter 12.
Example:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
printed result:
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Struct Methods
Just like in most ObjecOriented settings, Structs can also have Methods declared to them. Utilizing methods ensures that all the functionality related to a Struct is all logically bundled together in one place.
Methods are defined the same as functions, using the fn
keyword. However, to associate methods with a struct we need the impl
keyword.
Example:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle{
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect_one = Rectangle {
width: 1080,
height: 1920,
};
println!("The area of a rectangle is {} sqr pixels.", area(&rect_one));
}
This changes the signature for area()
it no longer needs parameters, utilizing a reference to the instance itself via &self
, short for self: &Self
. Very common in OOP languages and their method implementations.
To change the value of fields within the struct via a method, we use mut &self
and set the instance to mutable of course.
Methods must have a parameter named
self
of typeSelf
for their first parameter, so Rust lets you abbreviate this with only the nameself
in the first parameter spot.
Methods taking ownership of struct instances
Sometimes, although rarely you’ll encounter self
instead of &self
because there are cases where you’d want the method to take ownership in order to consume it and return something else while preventing the caller from using the original instance after.
Methods with Field names
Methods can be named after a struct’s field. This doesn’t confuse Rust because like function names, methods are followed by ()
and fields are not.
This is common for getters, methods that return the value of a field. Which is great for InformationHiding where the fields are private BUT the type’s API, the methods, are public.
Privacy is discussed in Chapter 7.
A comparison with C++ on calling methods
Goes over the
->
operator in C++ and why Rust opts for auto referencing and dereferencing in this case. Read here.
Associated functions
These are functions defined within a struct’s impl
block but don’t take a reference of &self
as a parameter.
// Resembling what's called a #ClassFunction in some OOP languages, I think #Python does this...
They’re often used for constructors that return new instances of the struct, usually named new
.
Example of a function that produces a square Rectangle
instance.
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
The Self
in the return type is an alias, no need to repeat Rectangle
…
Calling Associated function
Unlike a method which is called using dot notation,. these are called with ::
syntax. E.g. let sqr = Rectangle::square(4);
This function is namespaced by the struct: the
::
syntax is used for both associated functions and namespaces created by modules.
Multiple impl
blocks
A struct can several of these blocks, there’s no real reason to do this, since it’s all the same namespace but it’s a common practice when dealing with Generic Types and Traits.
Example:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Summary
- Structs allow us to bundle related data together into a single type that can be instantiated.
- There are regular Structs with fields and there are Tuple Structs without fields.
- We discussed Struct ownership.
- Structs can have methods that act on instances, defined in
impl
blocks. - Methods can mutate mutable structs and can also take ownership.
- Associated function are functions in the
impl
block of a struct that don’t take aself
or&self
parameter. - A struct can have multiple
impl
blocks.