Step 5 of 7
Getters, invariants and multiple impl blocks
A getter is a method that just returns a field. In Rust it's named after the field, with no get_ prefix: balance(), not get_balance() (that's the official API guideline, and the standard library follows it everywhere: v.len(), s.capacity()). A method can share its name with a field: hp.current is the field, hp.current() the method.
For a String field, return a borrowed &str so callers can look without copying: fn name(&self) -> &str { &self.name }. (&self.name is a &String, which turns into &str automatically, as in the slices module.)
Why not just read the field?
Inside one file you can. But at work, structs are shared between modules and teams, and their fields are usually private (the modules module shows how). Then the methods are the only way in, which lets the type keep its invariants: rules that are always true for every value of the type. "current never exceeds max." "A balance is never negative." "An email always contains @." If every change goes through a method that checks the rule, no code anywhere can break it.
struct Health {
current: u32,
max: u32,
}
impl Health {
fn new(max: u32) -> Self {
Self { current: max, max }
}
fn current(&self) -> u32 {
self.current
}
fn max(&self) -> u32 {
self.max
}
}
impl Health {
fn heal(&mut self, amount: u32) {
self.current = (self.current + amount).min(self.max);
}
fn damage(&mut self, amount: u32) {
self.current = self.current.saturating_sub(amount);
}
fn percent(&self) -> u32 {
self.current * 100 / self.max
}
}
fn main() {
let mut hp = Health::new(80);
hp.damage(50);
println!("{}/{} ({}%)", hp.current(), hp.max(), hp.percent());
hp.heal(100);
println!("{}/{} ({}%)", hp.current(), hp.max(), hp.percent());
}
30/80 (37%)
80/80 (100%)
- The invariant here is
current <= max.newstarts full,healcaps the result with.min(self.max)(a.min(b)is the smaller of two numbers), anddamagecan't go below zero. No method can break the rule. percentuses integer math: 30 × 100 / 80 is 37 (the .5 is dropped).
Multiple impl blocks
Notice there are two impl Health blocks. That's allowed, and it means exactly the same as one big block. Teams use it to group methods (constructors and getters here, game logic there). You'll see more of it later: generic code and traits each get their own impl blocks.
Your task
The User struct has a constructor. In a second impl block, add:
- getters
username(&self) -> &str,email(&self) -> &strandlogin_count(&self) -> u32, named after their fields (noget_prefix), record_login(&mut self): adds 1 to the login count,change_email(&mut self, email: &str) -> bool: the invariant is "an email contains@". If the new email has no@, change nothing and returnfalse; otherwise store it and returntrue.
Challenge 2
A volume control with the invariant "the level is never above 100". Write:
fn new(level: u8) -> Self: a level above 100 becomes 100.fn level(&self) -> u8: the getter.fn up(&mut self): adds 10, but never goes above 100.fn down(&mut self): subtracts 10, but never goes below 0.
Challenge 3
A shopping cart with a size limit. Write two impl blocks for Cart: one with the constructor and getters, one with the rest.
fn new(max_items: usize) -> Self: an empty cart.fn count(&self) -> usize: how many prices are in the cart.fn total(&self) -> u32: the sum of all prices.fn add(&mut self, price_cents: u32) -> bool: adds the price unless the cart already holdsmax_items; returns whether it was added.
Previous: self: methods that take ownership Next: Method chaining