Step 5 of 7
Dyn compatibility (object safety)
Not every trait can be used as dyn Trait. To call methods through a vtable, the compiler must be able to build that table without knowing the concrete type. A trait that allows it is dyn compatible (older docs and blog posts call it object safe).
trait Shape {
fn area(&self) -> f64;
fn scaled(&self, k: f64) -> Self;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.0 * self.radius * self.radius
}
fn scaled(&self, k: f64) -> Self {
Circle { radius: self.radius * k }
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })];
println!("{}", shapes.len());
}
error[E0038]: the trait `Shape` is not dyn compatible
|
| fn scaled(&self, k: f64) -> Self;
| ^^^^ ...because method `scaled` references the `Self` type in its return type
Through a dyn Shape, what would scaled return? A Circle? A Square? Its size isn't known at compile time, so there's no way to call it. The main rules, each with the same reason behind it:
- No
Selfin the return type (or in arguments other than the receiver): the concrete type is unknown.Cloneis the famous example:fn clone(&self) -> Self. - No generic methods like
fn log<T: Display>(&self, value: T): there would be one version perT, infinitely many, and a vtable can't hold them all. - No functions without
selflikefn new() -> Self: there's no value to find a vtable through. - The trait can't require
Sized, sotrait Shape: Cloneis not dyn compatible either (ClonerequiresSized).
The fixes
- Exclude a method from the vtable with
where Self: Sized. It can still be called on concrete types, just not throughdyn:
trait Shape {
fn area(&self) -> f64;
fn scaled(&self, k: f64) -> Self
where
Self: Sized;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.0 * self.radius * self.radius
}
fn scaled(&self, k: f64) -> Self {
Circle { radius: self.radius * k }
}
}
fn main() {
let big = Circle { radius: 1.0 }.scaled(2.0);
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 }), Box::new(big)];
for s in &shapes {
println!("{}", s.area());
}
}
3
12
Calling s.scaled(2.0) on a Box<dyn Shape> would now give error: the `scaled` method cannot be invoked on a trait object.
- Replace a generic parameter with a trait object:
fn log(&self, value: &dyn Display)instead offn log<T: Display>(&self, value: T). - Return a box instead of
Self: to clone trait objects, addfn clone_box(&self) -> Box<dyn Shape>and implement it asBox::new(self.clone()). - Split the trait: keep the dyn-friendly methods in one trait and put constructors or generic helpers in another.
Your task
Widget can't be used as dyn Widget, so render_all doesn't compile. Make it dyn compatible without removing any method: change with_label to take label: &dyn Display instead of a generic, and keep duplicate callable on concrete widgets with where Self: Sized. Update both impls to match.
Challenge 2
Clone isn't dyn compatible, so you can't call .clone() on a Box<dyn Shape>. The standard workaround is a clone_box method. Circle already has it; implement clone_box for Square, and write fn duplicate_all(shapes: &[Box<dyn Shape>]) -> Vec<Box<dyn Shape>> that returns a copy of every shape.
Challenge 3
A key-value Storage trait should work as &mut dyn Storage, but it also has a constructor fn new() -> Self, which makes it not dyn compatible. Fix the trait (keep new, but exclude it from the vtable), implement it for MemoryStorage (backed by a HashMap<String, String>) and NullStorage (stores nothing; get always returns None), and write fn save_all(store: &mut dyn Storage, pairs: &[(&str, &str)]).
Previous: Trait objects in structs: plugins Next: Enums or trait objects?