Structs
01/23/2025
word count:741
estimated reading time:4 minutes
Struct -
or a structure is a custom data type that lets you package together and name multiple related values that make up a meaningful group.
Defining and instantiating Structs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64
}
let user1 = User {
active: true,
username: String::from("someusername"),
email: ""'"""",
sign_in_count: 1,
};
Changing and accessing fields:
let mut user1 = User { // creating an instance of the struct
active: true,
username: String::from("someusername"),
email: "",
sign_in_count: 1,
};
user1.email = String::from("another-email.."); // since its mutable we can change the fields value
Tuple structs
struct Color(i32,i32,i32);
struct Point(i32,i32,i32);
Color and Point are different types, and cannot take each other as arguments.
Unit structs
struct AlwaysEqaul;
let subject = AlwaysEqual;
More on that later.
Using struct in a program:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn area(dimensions: &Rectangle) -> u32 {
dimensions.width * dimensions.height
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
print!("rect1 is {:#?}", rect1);
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
#[derive(Debug)]
let’s us opt in to pretty print the struct, using: {:?} and {:#?}
Another way to print out a value using the Debug format is to use the dbg!
macro, which takes ownership of an expression (as opposed to println which takes a ref), prints the file and line number of where that dbg! macro call occurs in the code along with the resultant value of that expression, and returns ownership of the value.
Note - dbg! prints to the stderr and println! prints to the stdout.
Using dbg!:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn area(dimensions: &Rectangle) -> u32 {
dimensions.width * dimensions.height
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
print!("rect1 is {:#?}", rect1);
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
output:
❯ cargo run
[src/main.rs:14:16] 30 * scale = 60
rect1 is Rectangle {
width: 60,
height: 50,
}
The area of the rectangle is 3000 square pixels.
Method Syntax
Methods are similar to functions, we declare them using the fn
keyword. Unlike functions, methods are define within the context of a struct (or enum or trait) and their first parameter is always self
.
Defining Methods
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
println!("rect area: {}", rect1.area());
}
Everything in the impl
block will be associated with the Rectangle
type. Notice we use (&self
) as the first and in this case the only parameter. This param refers to the Rectangle instance that was instantiated, in the above case its rect1
. the &
indicates that this method borrows the Self
instance
The main reason for using methods instead of functions, in addition to providing method syntax and not having to repeat the type of self
in every signature, is for organization. We’ve put all the things we can do with an instance of a type in one impl
block rather than making future users of our code search for capabilities of Rectangle
in various places.
Note we can choose to give a method the same name as one of the struct’s fields.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn width(&self) -> u32 {
self.width
}
}
Often, but not always, when we give methods with same name as a field we want it to only return the value, this methods are called getters
. They are useful because you can make the field private but the method public, and thus enable read only access to that field.
Lets define a new method that returns true if a self can hold another rectangle inside it or false otherwise:
fn can_hold(&self, other: &Rectangle) -> bool {
// takes immutable borrow of another rectangle
self.width > other.width && self.height > other.height
}
// calling the method:
rect1.can_hold(&rect2)
All functions inside an
impl
block are called associated functions
We have already used an assoicated function that is not a method (doesn’t take self) : String::from("hello")
Usually this functions are used as constructors that will return a new instance of the struct. for example:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(3);
The Self keyword in the return type and the body are an aliases for type of the imple block, in this case Rectangle.
Each struct is allowed to have multiple
impl
blocks. There is no reason to separate them but it is a valid syntax.