Step 1 of 7
dyn Trait: many types in one Vec
Generics are great when a function works with one type at a time. But a drawing app needs one list holding circles, rectangles and triangles together, and a Vec<T> has a single element type:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.0 * self.radius * self.radius
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }];
println!("{}", shapes.len());
}
error[E0308]: mismatched types
The answer is a trait object: dyn Shape means "some value of a type that implements Shape; which type exactly is only known at run time". Because those types have different sizes, a trait object always sits behind a pointer, most often a Box:
use std::f64::consts::PI;
trait Shape {
fn name(&self) -> String;
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rect {
w: f64,
h: f64,
}
impl Shape for Circle {
fn name(&self) -> String {
String::from("circle")
}
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
}
impl Shape for Rect {
fn name(&self) -> String {
String::from("rect")
}
fn area(&self) -> f64 {
self.w * self.h
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rect { w: 2.0, h: 3.0 }),
Box::new(Circle { radius: 2.0 }),
];
let mut total = 0.0;
for s in &shapes {
println!("{}: {:.2}", s.name(), s.area());
total += s.area();
}
println!("total: {:.2}", total);
}
circle: 3.14
rect: 6.00
circle: 12.57
total: 21.71
Piece by piece:
Box::new(value)moves the value to the heap and gives you aBox, an owning pointer to it. (The smart pointers module coversBoxin depth.) EveryBox<dyn Shape>is the same size, whatever shape is inside, so they fit side by side in aVec.Vec<Box<dyn Shape>>is "a vector of boxes, each holding someShape". The annotation is needed: it tells Rust to turn eachBox<Circle>andBox<Rect>into aBox<dyn Shape>.s.name()ands.area()call the right implementation for whatever is in each box, decided while the program runs. This is dynamic dispatch (step 3 shows how it works).
Through a dyn Shape you can call the trait's methods, and nothing else: the compiler no longer knows the concrete type.
Common mistakes
- Forgetting
dyn.Vec<Box<Shape>>giveserror[E0782]: expected a type, found a trait. Since the 2021 edition,dynis required. - No pointer.
Vec<dyn Shape>giveserror[E0277]: the size for values of type `dyn Shape` cannot be known at compilation time. Put it in aBox(or use&dyn Shape). - Calling a method that isn't in the trait. If
Circlehas its ownfn radius(&self), then on aBox<dyn Shape>you geterror[E0599]: no method named `radius` found for struct `Box<dyn Shape>`. Add the method to the trait if every shape needs it.
Your task
Implement Shape for Square (name square, area side * side), and write fn total_area(shapes: &[Box<dyn Shape>]) -> f64 that adds up the areas of a mixed list.
Challenge 2
Write fn describe_all(shapes: &[Box<dyn Shape>]) -> Vec<String> that returns one line per shape, <name>: <area with 2 decimals>, like rect: 6.00.
Challenge 3
An alerting system can send a message through several channels. Write a trait Notifier with one method fn send(&self, msg: &str) -> String that returns what would be sent, and implement it:
Email:email to <address>: <msg>Sms:sms to <number>: <msg>, but SMS messages are cut to their first 20 charactersSlack:#<channel>: <msg>
Then write fn notify_all(notifiers: &[Box<dyn Notifier>], msg: &str) -> Vec<String>.