Phase 3Data structures

#9 Structs

struct, impl, methods

Defining structs

A struct (short for structure) lets you group related data together under a single name. Each piece of data is called a field:

Struct definition
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

Structs are similar to tuples in that they hold multiple values, but each field has a name, making the data self-documenting.

Creating instances

To create an instance of a struct, specify values for every field:

Creating struct instances
let user1 = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    active: true,
    sign_in_count: 1,
};

// Access fields with dot notation
println!("Username: {}", user1.username);

// Struct update syntax — create a new struct from an existing one
let user2 = User {
    email: String::from("bob@example.com"),
    ..user1  // Fill remaining fields from user1
};

With field init shorthand, if a variable has the same name as a field, you can write just the name:

Field init shorthand
fn build_user(email: String, username: String) -> User {
    User {
        username,           // Same as username: username
        email,              // Same as email: email
        active: true,
        sign_in_count: 1,
    }
}

Methods with impl

Methods are functions defined within an impl block. Their first parameter is always &self (or &mut self), which refers to the struct instance:

Methods
#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

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

fn main() {
    let rect = Rectangle { width: 30.0, height: 50.0 };
    println!("Rectangle: {rect:?}");
    println!("Area: {}", rect.area());

    let small = Rectangle { width: 10.0, height: 20.0 };
    println!("Can hold: {}", rect.can_hold(&small));
}

The #[derive(Debug)] attribute automatically implements the Debug trait, letting you print the struct with {:?} or {:#?} for pretty printing.

Associated functions

Functions in an impl block that do not take self are called associated functions. They are often used as constructors:

Associated function (constructor)
impl Rectangle {
    fn square(size: f64) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

// Call with :: syntax
let sq = Rectangle::square(10.0);
println!("Square: {sq:?}");

You call associated functions using the :: syntax, similar to String::from() or Vec::new().

Tuple structs

Tuple structs have a name but their fields are not named. They are useful for creating distinct types:

Tuple structs
struct Color(u8, u8, u8);
struct Point(i32, i32, i32);

let orange = Color(255, 128, 0);
let origin = Point(0, 0, 0);

// Access by index
println!("Red: {}", orange.0);
println!("Y: {}", origin.1);

// Color and Point are different types,
// even though both hold three values

Even if two tuple structs have the same field types, they are different types. A function that takes a Color will not accept a Point.

Your turn

Try running the structs demo with cargo run and cargo check in the terminal below:

terminal — cargo
user@stemlegacy:~/structs-demo$