5 - Using Structs to Structure Related Data
5.1 - Defining and Instantiating Structs
If you're coming from Go, then a struct in Rust is very similar to a struct in Go. It has public and private fields and you can define methods on a struct. If you're coming from JavaScript or Java, then a struct in Rust is similar to a class, except that a struct can't inherit from another struct. In any of these languages, a trait is very similar to an interface
.
If you're coming from C/C++, then a Rust struct is sort of like a C struct except you can add methods to it like a C++ class. If you're coming from some other language, I'm going to assume the concept of a struct
or "object" isn't totally foreign to you.
Here's what a struct looks like in Rust:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// Create an instance of User
let mut myUser = User {
active: true,
username: String::from("jwalton"),
email: String::from("jwalton@example.com"),
sign_in_count: 1,
};
// Access fields with `.`
println!("Name: {}", myUser.username);
// Variable must be declared as `mut` if we want to be
// able to modify the structure.
myUser.email = String::from("other_email@example,com");
}
Note if you want to modify the contents of a struct, is has to be marked as mut
. You can't mark individual fields as mutable - either the whole structure is, or none of it is.
If you're curious about how Rust structures are laid out in memory, check the Rust Reference's section on Type Layout.
Using the Field Init Shorthand
Much like in JavaScript, we can initialize fields with a shorthand:
fn build_user(email: String, username: String) -> User {
User {
active: true,
// Instead of `username: username,` we can do:
username,
email,
sign_in_count: 1,
}
}
Creating Instances from Other Instances with Struct Update Syntax
Rust has something called the struct update syntax which allows us to copy fields from another struct (and which is very similar to the spread operator in JavaScript). This example will set the email of user2
, and then copy all other fields from user1
:
let user2 = User {
email: String::from("yet_another_email@example.com"),
..user1
}
When you store a field in a structure, or use the struct update syntax as in this example, from an ownership perspective you are moving that field. In this example, once we create user2
, we can no longer use user1
because its username field has been moved. If we had given user2
an email
and a username
, then all the remaining fields we assigned from user1
would be basic data types that implement the Copy
trait. In this case, nothing would move, so user1
would still be valid.
Using Tuple Structs Without Named Fields to Create Different Types
Tuple structs are basically named tuples:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Note here that Color
and Point
are two different types, even though they have the same structure. If you have a function that accepted a Color
, the compiler will complain if you try to pass in a Point
.
Unit-Like Structs Without Any Fields
You can define a struct without any fields. These are used when you want to implement some trait (see chapter 10) but you don't have any data you actually want to store in your struct:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
You don't even need the {}
to create an instance of AlwaysEqual
.
Ownership of Struct Data
In the User
struct above, we used a String
type in the struct for the username
and email
. This means that the struct owns that String
. When the struct is dropped, those two strings will be dropped too. We could instead have used an &String
or &str
, in which case the struct would store a reference to the string, and wouldn't own the string directly. The struct would be borrowing the string. We're not going to show an example of this though, because to do this we need something called a lifetime annotation, which we'll talk about in chapter 10.
5.2 - An Example Program Using Structs
A quick example of a program that uses a struct:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
area
takes an immutable reference to the Rectangle
struct. We know when we call area
, it won't modify our struct (even if rect1
was declared as mutable in the caller). Passing a reference means the caller will retain ownership. Also, accessing fields on the borrowed struct doesn't move them.
Adding Useful Functionality with Derived Traits
It would be cool if we could "print" a rectangle:
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("A rectangle: {}", rect1); // This will error.
In Java and JavaScript we could to this with a toString
method. In Go we could implement the Stringer
interface. In Rust we have two different traits we can implement: std::fmt::Display
and Debug
. The Debug trait is one that's intended, as the name suggests, for debugging and it's the one we want here.
Instead of implementing this trait ourselves, we can derive this trait, which is a fancy way of saying we can let Rust generate the code for this trait for us:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("A rectangle: {:?}", rect1);
}
If you run this, it will print:
A rectangle: Rectangle { width: 30, height: 50 }
The placeholder in println!
has changed from {}
to {:?}
, which lets println!
know we want the debug output format. We could also use {:#?}
to "pretty print" the output.
There's also a handy macro called dbg!
which will pretty-print the value, and the file name and source line. dbg!(&rect1);
would print something like:
[src/main.rs:13] &rect1 = Rectangle {
width: 30,
height: 50,
}
Note that unlike println!
, dbg!
takes ownership of the value passed in, so we pass a reference to rect1
instead of passing rect1
directly to prevent this. There are a number of other derivable traits - see Appendix C. And, again, to learn more about traits see chapter 10.
5.3 - Method Syntax
Methods are functions defined on a struct. Their first parameter is always self
, which represents the instance of the struct the method is being called on (similar to this
in other languages).
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
// Returns true if `other` Rectangle can fit inside this one.
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
The impl
(implementation) block defines methods and associated functions on the Rectangle type. area
takes a reference to &self
, which is actually a short form for self: &Self
.
A method will generally have one of three different parameters for self
:
fn method(&self)
is a method with an immutable borrow of self. Like any other parameter, it is immutable by default.fn method(& mut self)
is a method with a mutable borrow of self, which means this method can change fields onself
.fn method(mut self)
is a method that takes ownership of self. These methods you won't see as often, because once you call such a method the receiver is no longer valid, but these methods are often used to transform a value into some other structure. An example is themap
method on iterator, which destroys the original iterator and returns a new one.
You can have a method on a struct with the same name as one of the fields. This is most commonly used to add a getter method to a struct. You can make it so a rectangle has a private width: u32
field, and a public width(): u32
method, which effectively makes width
read-only. (What are public and private fields and methods? You'll have to wait for chapter 7.)
Automatic Referencing and Dereferencing
You may have noticed that area
takes a ref to self, but we called it as rect1.area()
and not (&rect1).area()
. Much like in Go, Rust has automatic referencing and dereferencing. When you call a method on a struct, Rust will automatically add in the &
, &mut
, or *
so the object matches the signature of the method.
Continue to chapter 6.