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?
- A. It prints
1200and then30. - B. It fails because struct fields are private.
- C. It fails because
areatakes ownership ofposter. - D. It prints
1200twice.
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.