Skill 13 · Rust Best Practices
Subchapter 13.6
references/chapter_06.mdMarkdown7 KBView on GitHub
Static where you can, dynamic where you must
Rust allows you to handle polymorphic code in two ways:
Understanding the trade-offs lets you write faster, smaller and more flexible code.
Every programming language has tools for effectively handling the duplication of concepts. In Rust, one such tool is generics: abstract stand-ins for concrete types or other properties. We can express the behavior of generics or how they relate to other generics without knowing what will be in their place when compiling and running the code.
We use generics to create definitions for items like function signatures or structs, which we can then use with many different concrete data types. Let’s first look at how to define functions, structs, enums, and methods using generics. Generics can also be used to implement Type State Pattern and constrain a struct functionality to certain expected types, more on type state on Chapter 7.
You might be wondering whether there is a runtime cost when using generic type parameters. The good news is that using generic types won’t make your program run any slower than it would with concrete types. Rust accomplishes this by performing monomorphization of the code using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. The compiler checks for all occurrences of the generic parameter and generates code for the concrete types the generic code is called with.
A static dispatch is basically a constrained version of a generics, a trait bounded generic, at compile-time it is able to check if your generic satisfies the declared traits.
fn specialized_sum<T: MyTrait, U: Iterator<Item = T>>(iter: U) -> T {
iter.map(|x| x.random_mapping()).sum()
}
// or, equivalent, more modern
fn specialized_sum<T: MyTrait>(iter: impl Iterator<Item = T>) -> T {
iter.map(|x| x.random_mapping()).sum()
}This is compiled into specialized machine code for each usage, fast and inlined.
Usually dynamic dispatch is used with some kind of pointer or a reference, like Box<dyn Trait>, Arc<dyn Trait> or &dyn trait.
❗ Closer to what you would get in an object oriented language and can have some heavy costs associated to it. Can avoid generic entirely and let you mix types that implement the same traits.
trait Animal {
fn greet(&self) -> String;
}
struct Dog;
impl Animal for Dog {
fn greet(&self) -> String {
"woof".to_string()
}
}
struct Cat;
impl Animal for Cat {
fn greet(&self) -> String {
"meow".to_string()
}
}
fn all_animals_greeting(animals: Vec<Box<dyn Animal>>) {
for animal in animals {
println!("{}", animal.greet())
}
}| Static Dispatch (impl Trait) | Dynamic Dispatch (dyn Trait) | |
|---|---|---|
| Performance | ✅ Faster, inlined | ❌ Slower: vtable indirection |
| Compile time | ❌ Slower: monomorphization | ✅ Faster: shared code |
| Binary size | ❌ Larger: per-type codegen | ✅ Smaller |
| Flexibility | ❌ Rigid, one type at a time | ✅ Can mix types in collections |
| Use in trait fn() | ❌ Traits must be object-safe | ✅ Works with trait objects |
| Errors | ✅ Clearer | ❌ Erased types can confuse errors |
Box<dyn Trait> when flexibility outweighs speed.Favor static dispatch until your trait needs to live behind a pointer.
Dynamic dispatch Ptr<dyn Trait> is a powerful tool, but it also has significant performance trade-offs. You should only reach for it when type erasure or runtime polymorphism are essential. It is important to know when you need Trait Objects:
fn all_animals_greeting(animals: Vec<Box<dyn Animal>>) {
for animal in animals {
println!("{}", animal.greet())
}
}&dyn Trait over Box<dyn Trait> when you don’t need ownership.Arc<dyn Trait> for shared access across threads.dyn Trait if the trait has methods that return Self.// ✅ Use generics when possible
struct Renderer<B: Backend> {
backend: B
}
// ❌ Premature Boxing
struct Renderer {
backend: Box<dyn Backend> // Boxing too early
}dyn trait in a public API, Box at the boundary, not internally.dyn Traits from object-safe traits:
Self: Sized.&self, &mut self or self.// ✅ Object Safe
trait Runnable {
fn run(&self);
}
// ❌ Not Object Safe
trait Factory {
fn create<T>() -> T; // generic methods are not allowed
}