Step 2 of 7
&dyn Trait and Box<dyn Trait>
A trait object can sit behind any kind of pointer. The two you'll use most:
| Form | Meaning | Use it for |
|---|---|---|
&dyn Trait |
borrows some value that implements the trait | parameters that only use the value |
&mut dyn Trait |
borrows it mutably | parameters that change it (&mut dyn std::fmt::Write) |
Box<dyn Trait> |
owns it, on the heap | storing in a Vec or a struct field, returning from a function |
You've already met one: Box<dyn Error> from the custom errors module is a boxed trait object.
trait Shape {
fn name(&self) -> String;
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Shape for Circle {
fn name(&self) -> String {
String::from("circle")
}
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
impl Shape for Square {
fn name(&self) -> String {
String::from("square")
}
fn area(&self) -> f64 {
self.side * self.side
}
}
fn report(s: &dyn Shape) -> String {
format!("{} with area {:.1}", s.name(), s.area())
}
fn make_shape(kind: &str, size: f64) -> Box<dyn Shape> {
if kind == "circle" {
Box::new(Circle { radius: size })
} else {
Box::new(Square { side: size })
}
}
fn main() {
let c = Circle { radius: 1.0 };
println!("{}", report(&c));
let shapes = vec![make_shape("square", 3.0), make_shape("circle", 2.0)];
for s in &shapes {
println!("{}", report(s.as_ref()));
}
}
circle with area 3.1
square with area 9.0
circle with area 12.6
report(&c): a&Circleturns into a&dyn Shapeautomatically. NoBox, no heap: the circle stays where it is.make_shapereturns aCircleor aSquare, decided at run time.-> impl Shapecan't do that (it stands for one type; you saw the E0308 in the traits module).Box<dyn Shape>can.- The
Vechere gets its type frommake_shape, so no annotation is needed.
From a Box to a &dyn
To pass a boxed shape to report, borrow the shape inside the box: s.as_ref() or &**s (one * for the &, one for the Box). Writing report(s) or report(&boxed) doesn't work, and the error is confusing:
trait Shape {
fn area(&self) -> f64;
}
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn report(s: &dyn Shape) -> f64 {
s.area()
}
fn main() {
let b: Box<dyn Shape> = Box::new(Square { side: 2.0 });
println!("{}", report(&b));
}
error[E0277]: the trait bound `Box<dyn Shape>: Shape` is not satisfied
Rust tries to treat the Box itself as the shape, and a Box doesn't implement Shape. report(b.as_ref()) hands over the square inside.
Common mistakes
- Returning
dyn Shapewithout a pointer.fn make() -> dyn Shapegiveserror[E0746]: return type cannot be a trait object without pointer indirection. ReturnBox<dyn Shape>. - Returning
&dyn Shapeto something created in the function. The value would be dropped at the end of the function (E0515 from the lifetimes module). A new value is returned in aBox;&dynis for borrowing what already exists. - Boxing when a reference would do.
fn report(s: Box<dyn Shape>)takes ownership and forces callers to allocate. If the function only reads the value, take&dyn Shape(or a generic, step 3).
Your task
Write fn parse_shape(line: &str) -> Option<Box<dyn Shape>> that turns a line of a drawing file into a shape: circle <radius>, rect <w> <h> or square <side> (use the Rect type for squares too). Return None for anything else, including bad numbers.
Challenge 2
Write fn biggest(shapes: &[Box<dyn Shape>]) -> Option<&dyn Shape> that returns a reference to the shape with the largest area (the first one on a tie), or None for an empty list. Return the shape inside the box with .as_ref().
Challenge 3
std::fmt::Write is the standard trait for "something you can write text into": a String implements it, and so can your own types. Write fn render_receipt(out: &mut dyn std::fmt::Write, items: &[(&str, u32)]) -> std::fmt::Result that writes one line per item, <name>: <dollars>.<cents>, then total: <dollars>.<cents>, each followed by a newline. Prices are in cents. Use writeln!(out, ...)?.
Previous: dyn Trait: many types in one Vec Next: Static vs dynamic dispatch