Structs in Rust
Enums and Structs are the building blocks for creating new types.
Structs allow us to group related data together. Think about them as object attributes/class attributes in object-oriented programming.
Using Structs to Structure Related Data
Using Structs
Let's start with creating a struct for a user.
struct User {
username: String, // we want our fields to actually own string data
email: String,
sign_in_count: u64,
active: bool
}
fn main() {
// create new instance of `User`
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("Happy"),
sign_in_count: 1
active: true,
}
// access the fields using dot notation
let name = user1.username;
// We can modify specific values in our struct using dot notation
// But make sure the instance itself is mutable
user1.username = String::from("wallabag");
}
Function Contructors
We can also use functions to construct new instances of User
.
fn main() {
let user2 = build_user(
String::from("[email protected]"),
String::from("Kevin")
);
}
fn build_user(email: String, username: String) -> User {
User {
email, // field init shorthand syntax
username,
sign_in_count: 1,
active: true
}
}
Reusing Instance Data
Create new instances, using existing instances
fn main() {
let user3 = User {
email: String::from("[email protected]"),
username: String::from("daniel"),
..user2 // all other fields assigned from `user2`
}
}
Tuple Structs
Create structs without named field, called tuple structs. Useful when we want our enture tuple to have a name and be of different type than other tuples.
fn main() {
// both color and point have the same 3 field types
// so, if a function expects a tuple type of Color,
// we can't pass it a tuple of `Point` and vice versa
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
}
Example Use Cases
We'll rewrite this program that calculates the area of rectangle, to understand Structs.
fn main() {
let width1 = 30;
let heigh1 = 50;
printlln!(
"The area of the rectangle is {} square units.").
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
This program works, but could be improved. Let's start grouping the width
and height
variable which are related.
fn main() {
let rect = (30, 50);
printlln!(
"The area of the rectangle is {} square units.").
area(rect)
);
}
fn area(dimesions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
This also works fine. However it's not clear what the fields in the tuple represent. Let's use Structs.
struct Rectangle {
width: u32,
height: u32
}
fn main() {
let rect = Rectangle { width: 30, height: 50};
printlln!(
"The area of the rectangle is {} square units.").
area(&rect)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Derived Traits It would be nice to see what our rectangle instances look like. What we want is something like:
But this doesn't work, because since our custom type doesn't implementstd::fmt::Display
trait, which specifies how something should be printed.
We can however use {:?}
or {:#?}
(which prints every field on newline) for pretty print, but requires Debug
trait to be implemented for the custom type, helpful for developers. We can do this by adding #[derive(Debug)]
or manually implement Debug
trait for the struct.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32
}
fn main() {
let rect = Rectangle { width: 30, height: 50};
println!("rect: {:?}", rect);
...
Method Syntax
How can we tie closely related functions to an instance of struct, named methods, instead of defining separately.
This requires us to use impl
block which houses methods for a particular Struct.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50};
printlln!(
"The area of the rectangle is {} square units.").
rect.area()
);
}
Note
- The first argument in a method is always
self
which is the instance the method is being called. We can also take a mutable reference, or in rare cases we can can take ownership. - Rust has a feature called autmatic referencing or derefering, which allows us to use dot notation (instead of two different notations, like in C++) on the object directly or calling on the reference of the object.
Let's create a method that takes in multiple parameters, called can_hold
, which takes a reference to another rectangle and determine if the current rectangle instances can hold another rectangel inside itself.
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50};
let rect1 = Rectangle { width: 20, height: 40};
let rect2 = Rectangle { width: 40, height: 50}
println!("rect can hold rect1: {}", rect.can_hold(&rect1));
println!("rect can hold rect1: {}", rect.can_hold(&rect2));
}
Associated Functions
We can also define associated functions inside impl
block, but unlike methods, they are not tied to an instance of our struct (think of them static methods).
We can create a new impl
block to write a associated function that construct a square.
Note
- Notice, there is no
self
argument for associated functions. - Use
::
onRectangle
struct to call associated functions. - We can also use them to write constructors for that type.