16.xChapter 16 summary and quiz
The structural centerpiece of the language, in eleven lessons. Review, then a capstone that uses most of it.
Quick review
A trait defines shared behavior, method signatures a type implements with impl Trait for Type (16.1); the orphan rule requires you own the trait or the type. Trait methods can have default bodies that implementers inherit and may override, and defaults can call required methods, the "small required core plus shared defaults" pattern behind Iterator (16.2). Trait bounds (T: Trait) restrict a generic to types with a capability, finally making largest compile with T: PartialOrd (16.3); combine with +, tidy with where. impl Trait is shorthand for "some type with this trait," in argument position (like a bound) and return position (for unnameable types like closures), with the catch that a return-position impl Trait is one concrete type (16.4).
Deriving asks the compiler to write a trait: Debug, Clone, Copy, PartialEq/Eq, PartialOrd/Ord, Hash, Default, each requires all fields to implement it, and a trait is derivable when "do it per field and combine" is obviously right (16.5). Display is the non-derivable, human-facing {}, implemented by hand and unlocking to_string() free (16.6). Operators are traits in std::ops (Add, Mul, Index, Neg...), so overloading is just implementing a trait, one lesson where C++ needs sixteen (16.7). From/Into are the blessed conversions (implement From, get Into free), TryFrom the fallible version, and ? uses From to convert error types as it propagates (16.8).
Trait objects (dyn Trait, usually Box<dyn Trait> or &dyn Trait) give runtime polymorphism and let a collection hold mixed types sharing a trait, via dynamic dispatch through a vtable, the runtime counterpart to generics' compile-time static dispatch (16.9). And Rust has no inheritance: composition reuses data, default trait methods reuse behavior, trait objects give polymorphism, the three jobs inheritance bundled, now separate (16.10).
Quiz time
Question #1
What's the difference between static dispatch (generics with bounds) and dynamic dispatch (trait objects), and when do you need the latter?
Show solution
Static dispatch resolves the method at compile time and monomorphizes a copy per type (fast, larger binary, types fixed at compile time). Dynamic dispatch (dyn Trait) looks up the method at runtime via a vtable (one shared copy, tiny per-call cost, can hold mixed types). You need trait objects when a collection must hold different types sharing a trait, or when a type is chosen at runtime; otherwise prefer generics.
Question #2
For each, say whether you'd derive the trait or implement it by hand, and why:
a) Debug for a config struct.
b) Display for a Money type that should print $12.50.
c) PartialEq for a Color where two colors are equal if RGB matches, ignoring an attached name field.
Show solution
a) Derive: Debug has an obvious per-field form. b) Implement by hand: Display is human-facing and has no automatic form; you choose $12.50. c) Implement by hand: the derived PartialEq would compare all fields including name, but you want equality to ignore name, so the mechanical version is wrong for this type.
Question #3
Why does implementing From<Celsius> for Fahrenheit also let you write celsius_value.into()?
Show solution
Because the standard library has a blanket implementation that provides Into<B> for A whenever From<A> for B exists. So implementing From<Celsius> for Fahrenheit automatically gives Into<Fahrenheit> for Celsius, and celsius_value.into() (with the target type known from context) does the same conversion as Fahrenheit::from(celsius_value).
Question #4
The capstone. Define a Shape trait with a required area(&self) -> f64 and a default describe(&self) -> String that uses area. Implement it for a Circle { radius: f64 } and a Rectangle { width: f64, height: f64 }. Then build a Vec<Box<dyn Shape>> holding one of each, and print each shape's description. Derive Debug on the structs.
Show solution
trait Shape {
fn area(&self) -> f64;
fn describe(&self) -> String {
format!("a shape with area {:.2}", self.area())
}
}
#[derive(Debug)]
struct Circle {
radius: f64,
}
#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.14159 * self.radius * self.radius
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn describe(&self) -> String {
format!("a {}x{} rectangle, area {:.2}", self.width, self.height, self.area())
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];
for shape in &shapes {
println!("{}", shape.describe());
}
}a shape with area 12.57
a 3x4 rectangle, area 12.00
Design notes. Shape has one required method (area) and a default describe built on it (lesson 16.2). Circle implements only area and inherits the default describe; Rectangle overrides describe with a more specific message. The Vec<Box<dyn Shape>> holds both types together via trait objects (lesson 16.9), and the loop dispatches describe dynamically to each. The {:.2} is precision formatting from lesson 5.5. This one small program uses required and default trait methods, overriding, and trait objects, the heart of the chapter.
You've reached the structural summit. There's one piece of the reference machinery still implicit: every reference has a lifetime, and chapter 17 finally writes it down, paying lesson 10.8's IOU and explaining the <'a> you've been carefully avoiding since chapter 9.