Introduction
Understanding exactly which traits can be used as trait objects is critical for designing flexible APIs. This lesson explores the dyn compatibility rules (formerly called "object safety") in detail, including workarounds for common violations.
Key Concepts
- Dyn Compatibility: A trait is dyn-compatible when every method can be dispatched through a vtable without knowing the concrete type.
- Self: Sized Escape Hatch: Adding
where Self: Sizedto a method excludes it from the vtable, keeping the trait dyn-compatible. - Dispatchable Methods: Methods that can be called through a trait object because they do not depend on the concrete type's identity.
Real World Context
Library authors frequently need to make traits dyn-compatible so users can store heterogeneous collections. The Error trait, Iterator (via dyn Iterator<Item = T>), and GUI widget traits all need careful design to remain dyn-compatible.
Deep Dive
The Six Rules
A trait is dyn-compatible when:
- No
Self: Sizedsupertrait — The trait itself must not requireSelf: Sized. - No associated constants — Constants cannot be dispatched through a vtable.
- Every method has a receiver —
&self,&mut self,self,Box<Self>,Rc<Self>,Arc<Self>, orPin<&mut Self>. - No method returns
Self— The vtable cannot know the concrete return type's size. - No generic methods — Type parameters would require infinite vtable entries.
- No
-> impl Traitorasync fn— These hide the concrete return type, which cannot be dispatched dynamically.
The where Self: Sized Workaround
You can keep a trait dyn-compatible by excluding problematic methods:
rusttrait Cloneable { fn clone_box(&self) -> Box<dyn Cloneable>; // This method cannot be dispatched dynamically, // but the trait remains dyn-compatible fn into_owned(self) -> Self where Self: Sized; }
Methods with where Self: Sized are not available on dyn Cloneable, but all other methods are.
Making Non-Compatible Traits Usable
Wrap a non-compatible trait in a compatible one:
rust// Not dyn-compatible: returns Self trait Duplicatable { fn duplicate(&self) -> Self; } // Dyn-compatible wrapper trait DynDuplicatable { fn duplicate_boxed(&self) -> Box<dyn DynDuplicatable>; } impl<T: Duplicatable + Clone + 'static> DynDuplicatable for T { fn duplicate_boxed(&self) -> Box<dyn DynDuplicatable> { Box::new(self.clone()) } }
Static Methods and Constructors
Static methods (no self receiver) break dyn compatibility. Move them behind where Self: Sized:
rusttrait Factory { fn create() -> Self where Self: Sized; fn name(&self) -> &str; // This is fine for dyn dispatch }
Common Pitfalls
- Adding a generic method later — Adding
fn process<T>(&self, val: T)to a public trait is a breaking change if users depend ondyn Trait. - Forgetting about associated constants — Even a
const VERSION: u32 = 1;in a trait breaks dyn compatibility.
Best Practices
- Design traits for dyn compatibility from the start — If you expect users to create trait objects, follow the rules from day one.
- Use
where Self: Sizedliberally — It lets you add convenience methods without sacrificing dyn compatibility. - Test with
fn assert_dyn_compat(_: &dyn YourTrait) {}— A compile-time check that your trait remains dyn-compatible.
Summary
- Six rules determine dyn compatibility: no Sized supertrait, no associated constants, receiver on all methods, no Self returns, no generics, no impl Trait returns.
where Self: Sizedexcludes individual methods from the vtable.- Wrapper traits can make non-compatible traits usable as trait objects.
- Design public traits for dyn compatibility proactively.
Code Examples
// Keeping a trait dyn-compatible with where Self: Sized
trait Plugin {
fn name(&self) -> &str;
fn execute(&self);
// Excluded from dyn dispatch, but trait stays compatible
fn clone_plugin(&self) -> Self
where
Self: Sized;
}
// This compiles: Plugin is dyn-compatible
fn run_plugins(plugins: &[Box<dyn Plugin>]) {
for p in plugins {
println!("Running: {}", p.name());
p.execute();
// p.clone_plugin(); // Not available on dyn Plugin
}
}