Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Structuring Related Data with Structs

Structures

Structs are custom data types that let you group related data together under a single type. They are similar to structs/classes in other languages, but Rust structs only store data by themselves. Behavior is added separately through impl blocks.

There are three types of structs in Rust:

  1. Normal Structs:
// A normal struct is defined using the struct keyword
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

fn main() {
    let username = String::from("ali");

    // Instantiated like this
    let user1 = User {
    username, // field init shorthand syntax
    email: String::from("example@email.com"),
    active: true,
    sign_in_count: 1,
    };

    // Properties are accessed with the . syntax
    println!("{}", user1.email);

    // Struct .. syntax
    let user2 = User {
        email: String::from("example@email.com"),
        ..user1 // *moves* the rest from user1 to here
    }

}
  1. Tuple-Structs: Tuple structs are structs whose fields are unnamed. They behave like tuples but create a distinct type. They’re kind of like “Positional Records” in C# but the fields are unnamed.
// This is how we define tuple-structs
struct Rgb(u8, u8, u8);
struct Point(i32, i32, i32);

fn main() {
    // Instantiated like this:
    let black: Rgb = Rgb(0, 0, 0);
    let origin: Point = Point(0, 0, 0);

    // Access happens the same as it happens with tuples
    let r = black.0;
    let g = black.1;
    let b = black.2;
}
// Useful for when field names are unnecessary
// or you want lightweight wrappers around your values.
struct UserId(u64);
struct ProductId(u64);
  1. Unit-Like Structs: Unit-like structs are structs with no fields; useful to use as marker types, zero sized types, or implementing traits without storing state, etc. They’re called unit-like because they resemble the unit type “()”.
// Defined like this
struct Logger;
impl Logger { // More on this in the next section
    fn log(&self, msg: &str) {
        println!({msg});
    }
}

fn main() {
    // Instantiated like this
    let logger = Logger;
    logger.log("hello, world!");
}

Methods

Methods are functions associated with a type. They are defined inside impl blocks, take “self” as the first parameter, and are called with the dot ‘.’ syntax.

struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        return self.width * self.height;
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("The rectangle's area is {}", rect.area());
}

More on “self”:

“self” is shorthand for “self: Self”. The “Self” type itself is a placeholder for whatever type comes after the impl block.

#![allow(unused)]
fn main() {
struct Point(i32, i32, i32)
impl Point {
    fn print_point(self: &Self) /*Self = Point*/ {
        println!("{} {} {}", self.0, self.1, self.2);
    }
}
}

Rust supports three common forms of self:

struct Example {
    x: i32,
    y: i32,
}

impl example {
    // 1. Ownership consuming self
    fn method1(self) {};

    // 2. Immutable borrow
    fn method2(&self) {};

    // 3. Mutable borrow
    fn method3(&mut self) {};
}

fn main() {
    let e = Example {
        x: 12,
        y: 34,
    };

    // e becomes invalid after this. Because it was
    // moved and freed inside of method1().
    e.method1();
}

Associated Functions

Associated functions are functions tied to a type but without a self parameter. Associated functions are commonly used as constructors, utility functions, conversion helpers, factory methods, etc. They are similar to static methods in C#.

struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    // Takes no self
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    // Called like this
    let sq = Rectangle::square(10);
    // Instead of something sq.square()
}

Ownership & Structs

Structs fully participate in Rust ownership rules. Each field inside a struct owns its data unless the field itself is a reference.

struct User {
// Here, the struct owns the String.
    username: String,
}

fn main() {
    // When the struct is dropped, all owned
    // fields are dropped automatically.
    let user1 = User {
    username: String::from("ali"),
    };

    // Moving a struct moves all non-Copy fields;
    // after this point user1 is invalid
    // because ownership moved it to user2.
    let user2 = user1;

} // username String is dropped here

An important detail is partial moves.

struct Person {
    name: String,
    age: u32,
}

fn main() {
    let person = Person {
        name: String::from("Ali"),
        age: 22,
    };

    // Here, person.name is moved out.
    let name = person.name;

    // But this works with no error because
    // u32 implements Copy
    println!("{}", person.age);

    // As expected this does not work since
    // the person struct was partially moved.
    // aka. person does not own the value inside of
    // name property anymore.
    println!("{:?}", person);
}