Rust FAQ: Traits -- Dynamic Dispatch
PART11 -- Traits and Inheritance (Dynamic Dispatch)
Q56: What is a dyn Trait?
In Rust, a dyn Trait is a trait object, which is a way to refer to any type that implements a specific trait at runtime. It allows you to work with different types that share the same trait without knowing their concrete type at compile time. The dyn keyword indicates dynamic dispatch, meaning the exact method to call is determined at runtime using a vtable (a table of function pointers).
How it works:
- A trait object is created using a pointer, like
Box<dyn Trait>,&dyn Trait, or&mut dyn Trait. - It stores a pointer to the data and a pointer to the vtable, which maps the trait’s methods to the implementing type’s functions.
Example:
trait Speak { fn speak(&self) -> String; } struct Dog; struct Cat; impl Speak for Dog { fn speak(&self) -> String { String::from("Woof!") } } impl Speak for Cat { fn speak(&self) -> String { String::from("Meow!") } } fn main() { let animals: Vec<Box<dyn Speak>> = vec![ Box::new(Dog), Box::new(Cat), ]; for animal in animals { println!("{}", animal.speak()); // Prints: Woof!, then Meow! } }
Why use dyn Trait?
- Polymorphism: Treat different types uniformly if they implement the same trait.
- Flexibility: Store mixed types in collections (e.g.,
Vec<Box<dyn Speak>>) or pass them to functions. - Dynamic Behavior: Useful when the type is determined at runtime (e.g., plugins or user input).
Trade-offs:
- Requires heap allocation (e.g.,
Box) or references (&dyn Trait). - Small runtime cost due to dynamic dispatch (see Q57).
- Traits must be object-safe (no generic methods or
Selfconstraints).
Q57: What is dynamic dispatch? Static dispatch?
Dynamic dispatch and static dispatch are two ways Rust resolves which method to call when using traits.
-
Dynamic Dispatch:
- Resolves method calls at runtime using a vtable (a table of function pointers for the trait’s methods).
- Used with trait objects (
dyn Trait), likeBox<dyn Trait>or&dyn Trait. - Pros:
- Allows polymorphism: You can work with different types implementing the same trait.
- Useful for collections of mixed types or runtime flexibility.
- Cons:
- Small runtime overhead due to vtable lookups.
- Requires heap allocation or references.
- Example:
#![allow(unused)] fn main() { trait Draw { fn draw(&self); } struct Circle; impl Draw for Circle { fn draw(&self) { println!("Circle"); } } fn draw_shape(shape: &dyn Draw) { shape.draw(); // Resolved at runtime } }
-
Static Dispatch:
- Resolves method calls at compile time by generating specific code for each type (monomorphization).
- Used with generics (
T: Trait) orimpl Trait. - Pros:
- Faster: No runtime overhead, as method calls are direct.
- No need for heap allocation in many cases.
- Cons:
- Generates separate code for each type, increasing binary size.
- Can’t mix different types in a single collection without trait objects.
- Example:
#![allow(unused)] fn main() { fn draw_shape<T: Draw>(shape: &T) { shape.draw(); // Resolved at compile time } }
When to use:
- Use dynamic dispatch (
dyn Trait) for runtime flexibility or mixed-type collections. - Use static dispatch (
T: Trait) for performance-critical code or when types are known at compile time.
Example comparing both:
fn main() { let circle = Circle; draw_shape(&circle as &dyn Draw); // Dynamic dispatch draw_shape(&circle); // Static dispatch }
Q58: Can I override a non-dynamically dispatched method?
Yes, you can override a non-dynamically dispatched method in Rust, but the term “override” is a bit different from languages with class-based inheritance. In Rust, methods are resolved via static dispatch when using generics (T: Trait) or direct calls on concrete types, and you can provide specific implementations for each type. Since Rust doesn’t have traditional inheritance, “overriding” means implementing a trait’s method for a specific type, which is always possible.
How it works:
- For a trait method, each type implementing the trait provides its own version of the method.
- Static dispatch (used with generics or
impl Trait) ensures the compiler picks the right implementation at compile time.
Example:
trait Speak { fn speak(&self) -> String; } struct Dog; struct Cat; impl Speak for Dog { fn speak(&self) -> String { String::from("Woof!") } } impl Speak for Cat { fn speak(&self) -> String { String::from("Meow!") } } fn call_speak<T: Speak>(animal: &T) { println!("{}", animal.speak()); // Static dispatch, picks correct method } fn main() { let dog = Dog; let cat = Cat; call_speak(&dog); // Prints: Woof! call_speak(&cat); // Prints: Meow! }
- No dynamic dispatch here: The
call_speakfunction uses generics, so the compiler generates separate code forDogandCat, with no “overriding” at runtime. - Overriding-like behavior: Each type’s implementation of
speakis distinct, effectively “overriding” the trait’s method for that type.
Key Points:
- You can always provide a new implementation for a trait method for each type.
- If the trait has a default implementation, you can override it:
#![allow(unused)] fn main() { trait Speak { fn speak(&self) -> String { String::from("Hello!") } } impl Speak for Dog { fn speak(&self) -> String { String::from("Woof!") } // Overrides default } } - For dynamic dispatch (
dyn Trait), the vtable ensures the correct method is called at runtime, but this is still based on the type’s implementation, not a traditional override.
There’s no restriction on “overriding” methods in Rust, as each type’s implementation is independent.
Q59: Why do I get a warning about method hiding in trait implementations?
A method hiding warning in Rust occurs when a method in an impl block for a type has the same name as a trait method but doesn’t actually implement or override the trait’s method. This can happen if the method signatures don’t match exactly, causing the type’s method to “hide” the trait’s method, which might confuse users expecting the trait’s behavior.
Why it happens:
- Rust expects that when you implement a trait, methods with the same name as the trait’s methods are meant to fulfill the trait’s contract.
- If the signatures differ (e.g., different parameters or return types), Rust treats the type’s method as separate, “hiding” the trait’s method when called directly on the type.
Example:
trait Calculate { fn compute(&self, x: i32) -> i32; } struct Processor; impl Calculate for Processor { fn compute(&self, x: i32) -> i32 { x * 2 } } impl Processor { fn compute(&self, x: f64) -> f64 { x * 3.0 } // Warning: hides trait method } fn main() { let proc = Processor; println!("{}", proc.compute(5.0)); // Calls Processor’s method (f64) println!("{}", <Processor as Calculate>::compute(&proc, 5)); // Calls trait’s method (i32) }
- Warning: The
computemethod inimpl Processortakes anf64and returns anf64, which doesn’t match the trait’scompute(takesi32, returnsi32). Rust warns that this method hides the trait’s method. - Result: Calling
proc.compute(5.0)uses theProcessor’s method, not the trait’s, which might not be what you intended.
How to fix:
- Match Signatures: Ensure the method matches the trait’s signature to implement it:
#![allow(unused)] fn main() { impl Processor { fn compute(&self, x: i32) -> i32 { x * 3 } // Matches trait, no warning } } - Use a Different Name: If you want a separate method, give it a distinct name to avoid confusion:
#![allow(unused)] fn main() { impl Processor { fn compute_float(&self, x: f64) -> f64 { x * 3.0 } // No warning } } - Explicit Trait Call: If you keep the hiding method, use explicit trait syntax to access the trait’s method:
#![allow(unused)] fn main() { <Processor as Calculate>::compute(&proc, 5) }
Why Rust warns:
- Prevents accidental misuse where you expect the trait’s method but get the type’s method.
- Encourages clear, intentional code design.
- Avoids confusion in APIs where users might not realize a method doesn’t belong to the trait.
Best Practice:
- Match trait method signatures exactly when implementing traits.
- Use distinct names for non-trait methods to avoid hiding.
- Check warnings with
cargo buildorclippyto catch these issues early.