R Rust by Evidence PDF

Chapter 6 — Structs and Methods

Goal

Model one coherent thing, then attach operations that belong to that thing.

A struct names a collection of fields. Unlike a tuple, it records what each position means:

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

fn area(rect: Rectangle) -> u32 {
    rect.width * rect.height
}

fn main() {
    let poster = Rectangle { width: 30, height: 40 };
    println!("{}", area(poster));
    println!("{}", poster.width);
}

Prediction

What happens?

RevealC

Representative compiler evidence:

error[E0382]: use of moved value: `poster`
  |
  |     println!("{}", area(poster));
  |                         ------ value moved here
  |     println!("{}", poster.width);
  |                    ^^^^^^^^^^^^ value used here after move

The fields are accessible because this code is in the same module. The problem is the function parameter: rect: Rectangle consumes its argument.

Mental model

Working model: a struct is one value with named parts. Moving the struct moves the whole value unless the type implements Copy.

Precise model: methods are functions declared in an impl block. Their first parameter controls access. self consumes the value, &self borrows it for reading, and &mut self borrows it for mutation. Method-call syntax supplies that first argument and performs ordinary borrowing adjustments where possible.

The operation here only observes a rectangle, so make that visible in its signature:

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

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

    fn scale(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }

    fn square(size: u32) -> Self {
        Self { width: size, height: size }
    }
}

fn main() {
    let mut poster = Rectangle::square(30);
    poster.scale(2);
    println!("{} by {}; area {}", poster.width, poster.height, poster.area());
}

Output:

60 by 60; area 3600

area and scale are methods because they receive self. square is an associated function: call it with Rectangle::square, not with a value.

Simplest fix

Change a read-only parameter from Rectangle to &Rectangle, or make it an &self method. That avoids a move and states the actual need.

Tradeoff alternative: derive Copy and Clone when every field is cheaply copyable:

#[derive(Clone, Copy)]
struct Rectangle {
    width: u32,
    height: u32,
}

Then passing a rectangle copies it. This is convenient for small plain-data types, but it changes assignment semantics for the entire type. Do not add Copy merely to silence one move error; borrowing scales to structs that later contain a String or other owned resource.

Mini challenge

Make this compile without cloning and without changing label:

struct Package {
    label: String,
    delivered: bool,
}

fn mark_delivered(package: Package) {
    package.delivered = true;
}

fn main() {
    let mut parcel = Package {
        label: String::from("BX-17"),
        delivered: false,
    };
    mark_delivered(parcel);
    println!("{}: {}", parcel.label, parcel.delivered);
}

Answer

Borrow the package mutably, and make the parameter binding operate through that borrow:

struct Package {
    label: String,
    delivered: bool,
}

fn mark_delivered(package: &mut Package) {
    package.delivered = true;
}

fn main() {
    let mut parcel = Package { label: String::from("BX-17"), delivered: false };
    mark_delivered(&mut parcel);
    println!("{}: {}", parcel.label, parcel.delivered);
}

The exclusive borrow lasts for the call; afterward parcel is usable again.