shagag
Frontend Engineer
by shagag

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.