10.xChapter 10 summary and quiz
This chapter handed you the first half of the type-building toolkit. Quick review, then a quiz that builds a small type from scratch.
Quick review
A struct is a program-defined type that groups related values under one name (10.1). Types are named in UpperCamelCase, fields in snake_case, and the lints enforce it. A struct answers "what parts make up this thing?"; chapter 11's enums answer "which of these things is it?"
You define a struct with named fields and create an instance by giving every field a value; leaving one out is E0063, because a half-built value is uninitialized (10.2). Read and write fields with dot notation. Mutability attaches to the binding, not the field: there's no per-field mut, so the whole instance is mutable or none of it is. Two pieces of syntax cut the repetition (10.3): field init shorthand writes name for name: name when a variable matches, and struct update syntax (..base) fills remaining fields from another instance, moving (or copying) them per chapter 8's rules.
Beyond named-field structs there are tuple structs (struct Rgb(u8, u8, u8)), whose fields are positional, and unit structs (struct Marker;), which hold no data (10.4). A single-field tuple struct is the newtype pattern: wrapping a value to give it a distinct type, turning "this f64 is meters" from a comment into a compiler check.
A method is a function defined in an impl block that takes self and is called with a dot (10.5). The receiver is the chapter 9 parameter decision once more: &self to read, &mut self to modify in place, self to consume. An associated function has no self and is called on the type with :: (10.6). Rust has no constructors; ::new is just an associated function named by convention that returns a fresh instance, and #[derive(Default)] supplies a ::default() that zeroes every field.
A struct can't be printed with {} until you implement Display yourself (chapter 16), but #[derive(Debug)] enables {:?} and the pretty {:#?} for free (10.7). derive is the compiler writing obvious boilerplate; each name in the list is a trait, chapter 16's subject. Finally, a struct owns its fields (10.8): moving the struct moves them all, dropping it drops them all, and it's Copy only if every field is. Storing a reference in a struct needs a lifetime (E0106), which is why your structs hold owned fields like String until chapter 17. The rectangle project (10.9) walked the whole thing from loose variables to a type with methods.
Quiz time
Question #1
From memory: what's the difference between a method and an associated function, and how is each called?
Show solution
A method takes self (as &self, &mut self, or self) and is called on an instance with a dot: r.area(). An associated function takes no self and is called on the type with a path: Rectangle::new(...). Both live in impl blocks and belong to the type.
Question #2
Compiles or not? For each failure, name the cause.
a)
struct Point { x: f64, y: f64 }
fn main() {
let p = Point { x: 1.0 };
println!("{}", p.x);
}
b)
#[derive(Debug)]
struct P { x: f64 }
fn main() {
let p = P { x: 1.0 };
println!("{:?}", p);
}
c)
struct P { x: f64 }
fn main() {
let p = P { x: 1.0 };
p.x = 2.0;
println!("{}", p.x);
}
d)
#[derive(Debug)]
struct Note { body: String }
fn main() {
let a = Note { body: String::from("hi") };
let b = a;
println!("{:?} {:?}", a, b);
}Show solution
a) Refused, E0063: missing field y. Every field must be initialized. b) Compiles, prints P { x: 1.0 }. c) Refused, E0594: p isn't mut, so its field can't be assigned. d) Refused, E0382: let b = a; moves a (a Note holds a String, so it isn't Copy), and a is then read after the move. Fix with &a or a.clone() (after deriving Clone).
Question #3
Choose the receiver (&self, &mut self, self) or "associated function (no self)" for each:
a) Circle::unit() returning a circle of radius 1
b) a method returning the circle's circumference
c) a method that grows the radius by a given amount
d) a method that consumes the circle and returns its area, discarding the circle
Show solution
a) Associated function, no self: it builds a new instance. b) &self: reads only. c) &mut self: changes a field in place. d) self: consumes the instance (the circle is thrown away).
Question #4
The capstone. Define a BankAccount struct with an owner: String and a balance: u64 (cents, the lesson 6.x "count money in integer cents" rule). Give it: an associated function new(owner: String) -> BankAccount starting at zero balance; a deposit(&mut self, cents: u64) method; a withdraw(&mut self, cents: u64) method that only subtracts if the balance is sufficient (otherwise leaves it unchanged); and a summary(&self) -> String returning something like "Ada: 1250 cents". Derive Debug. In main, open an account, deposit 2000, withdraw 750, attempt to withdraw 9999 (should be refused), and print the summary.
Show solution
#[derive(Debug)]
struct BankAccount {
owner: String,
balance: u64,
}
impl BankAccount {
fn new(owner: String) -> BankAccount {
BankAccount { owner, balance: 0 }
}
fn deposit(&mut self, cents: u64) {
self.balance += cents;
}
fn withdraw(&mut self, cents: u64) {
if cents <= self.balance {
self.balance -= cents;
}
}
fn summary(&self) -> String {
format!("{}: {} cents", self.owner, self.balance)
}
}
fn main() {
let mut account = BankAccount::new(String::from("Ada"));
account.deposit(2000);
account.withdraw(750);
account.withdraw(9999); // refused: balance unchanged
println!("{}", account.summary());
}Ada: 1250 cents
Design notes. new is an associated function (no self) using field init shorthand for owner. deposit and withdraw take &mut self because they change the balance, so account must be a mut binding. withdraw guards with if cents <= self.balance, so the 9999 attempt does nothing rather than underflowing a u64 (which would panic in debug, lesson 4.4). summary takes &self and uses format! (lesson 5.5) to build an owned String. The whole type owns its data, so it could move into a function or a collection without dragging a lifetime along.
If you reached for f64 to hold dollars, reread lesson 6.x: money in floats accumulates rounding error (lesson 4.5). Integer cents is the rule.
You can now invent types whose parts coexist. Chapter 11 gives you the other kind: enums, where a value is exactly one of several alternatives, where Option retires the null-pointer mistake, and where match becomes the most powerful tool in the language.