Rust by Example: Structs

In Rust we're able to define structs, which are a way to group related data together. Associated functions to structs are used to specify behavior for the structs. There are three types of structs in Rust: named-field structs, tuple structs, and unit structs.

Run code Copy code
#![allow(unused)]
fn main() {

Unit-like structs have no fields and are useful for implementing a trait on a type without storing any data in the type itself.

    struct UnitLike;

Tuple-like structs have unnamed fields and are useful for giving a tuple a name, making it a distinct type from other tuples where naming the fields would be redundant.

    struct Color(u8, u8, u8);
    let black = Color(0, 0, 0);

Defining a Named-Field struct, with fields and methods. The naming convention for structs is CamelCase.

    struct Rectangle {
        width: u32,
        height: u32,
    }

To define associated functions for a struct, use the impl keyword followed by the struct's name and a block containing the functions. These are called associated functions and follow the snake_case naming convention. Associated functions that don't take a struct instance as a parameter are called type-associated functions, commonly used as constructors or utility functions.

    impl Rectangle {
        fn new(width: u32, height: u32) -> Rectangle {
            Rectangle { width, height }
        }

Associated functions that take self as the first parameter are called methods. The self argument represents the instance of the struct the method is called on. It doesn't require a type annotation since Rust infers it. Using self means the method takes ownership of the instance, consuming it when called.

        fn area(self) -> u32 {
            self.width * self.height
        }

In this example, the method borrows the instance by using &self, so the instance is not consumed when the method is called. Use this when a method only needs to read, not modify, the instance.

        fn can_hold(&self, other: &Rectangle) -> bool {
            self.width > other.width 
                && self.height > other.height
        }

In this example, the method borrows the instance by using &mut self, allowing the method to modify the instance without consuming it. Use this when a method needs to make changes to the instance.

        fn increase_size(&mut self, width: u32, height: u32) {
            self.width += width;
            self.height += height;
        }

We can also return Self from a method, which is useful for chaining method calls.

        fn set_width(mut self, width: u32) -> Self {
            self.width = width;
            self
        }
    }

Creating an instance of the Rectangle struct

    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

Creating an instance of the Rectangle struct using the associated function new.

    let rect2 = Rectangle::new(30, 50);

Create a struct by using struct update syntax to use the fields of another instance of the same struct.

    let rect3 = Rectangle { width: 30, ..rect1 };

Accessing fields of a struct, using the dot notation

    println!("The area of the rect1 is {}", rect1.area());

Structs are private by default and only visible within the module they are declared in. Use the pub keyword to make a struct public. The same applies to its fields, which are also private by default. Other modules can use the struct and its public associated functions but cannot access private fields by name or use struct expressions to create new values.

    pub struct Person {
        pub name: String,
        age: u8,
    }

Rust allows multiple impl blocks for a struct, which is useful for separating methods into different files for large structs. Here we define a separate impl block for the Rectangle struct that we defined earlier.

    impl Rectangle {
        fn print(&self) {
            println!(
                "Rectangle: {} x {}", self.width, self.height,
            );
        }
    }

We can set default values for struct fields using the Default trait.

    #[derive(Default)]
    struct Point {
        x: i32,
        y: i32,
    }

We can instantiate a struct with default values using the Default trait.

    let point = Point::default();

We can also override some of the default values, when instantiating the struct.

    let other_point = Point {
        x: 10,
        ..Point::default()
    };

We can also implement the Default trait for our own structs. This is useful when we want to provide a custom default values.

    impl Default for Rectangle {
        fn default() -> Rectangle {
            Rectangle {
                width: 10,
                height: 10,
            }
        }
    }
}
$ rustc structs.rs
$ ./structs
The area of the rect1 is 1500
Go to Index | Next: Traits