← thecodex.expert · The Codex Family of Knowledge
Rust

Traits and Trait Objects

Rust gives you a choice most languages don't: static dispatch (zero cost, resolved at compile time) or dynamic dispatch (runtime flexibility, a small performance cost) — and you decide per use, not per language.

Rust 1.95+ dyn Trait Last verified:
Where this bites you

When a type has its own method with the same name as a trait method, value.method() picks the type's own (inherent) one — and a Box<dyn Trait> of the very same value picks the trait's. The call syntax is identical, the data is identical, and the output differs depending only on the static type you happen to be holding. Nothing warns you.

Rustshadowed_method_bug.rs
struct Circle { radius: f64 }

impl Circle {
    fn describe(&self) -> String {          // inherent method, same name as the trait's
        format!("Circle of radius {}", self.radius)
    }
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

let c = Circle { radius: 2.0 };
println!("{}", c.describe());                  // Circle of radius 2

let boxed: Box<dyn Shape> = Box::new(Circle { radius: 2.0 });
println!("{}", boxed.describe());              // This shape has area 12.57
Canonical Definition

A trait defines a set of methods a type can implement, similar in spirit to an interface. Using a trait as a generic bound (fn foo<T: Trait>) resolves at compile time via monomorphization — static dispatch, with zero runtime cost. A trait object, written dyn Trait, instead stores a pointer to the data plus a pointer to a table of its methods (a vtable), resolved at runtime — dynamic dispatch, which allows a single collection to hold several different concrete types, at a small runtime cost per call.

Defining and implementing a trait

A trait can include a default method implementation, which an implementing type can use as-is or override.

Rusttraits.rs
trait Shape {
    fn area(&self) -> f64;

    fn describe(&self) -> String {   // default method
        format!("This shape has area {:.2}", self.area())
    }
}

struct Circle { radius: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    // describe() uses the default — not overridden
}

Static dispatch: generics with trait bounds

Using a trait as a generic bound resolves at compile time — the compiler generates a separate, fully specialized version for each concrete type used, exactly like the generics covered earlier.

Ruststatic_dispatch.rs
fn print_area<T: Shape>(shape: &T) {   // resolved at compile time
    println!("{}", shape.describe());
}

Dynamic dispatch: dyn Trait and trait objects

A collection needing to hold several different concrete types behind one interface uses dyn Trait — usually behind a Box, since a trait object's exact size isn't known at compile time.

Rusttrait_objects.rs
struct Square { side: f64 }
impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
}

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 2.0 }),
    Box::new(Square { side: 3.0 }),
];

for shape in &shapes {
    println!("{}", shape.describe());   // resolved at RUNTIME via vtable
}

Inherent methods always win over trait methods when called on the concrete type, while a trait object can only see the trait's. Avoid the collision by giving the inherent method a distinct name; if you need to reach the trait's version on the concrete type, name the trait explicitly:

Rustshadowed_method_fix.rs
impl Circle {
    fn label(&self) -> String {             // distinct name: no shadowing
        format!("Circle of radius {}", self.radius)
    }
}

let c = Circle { radius: 2.0 };
println!("{}", c.label());                 // Circle of radius 2
println!("{}", Shape::describe(&c));       // This shape has area 12.57  (trait method, explicitly)

let boxed: Box<dyn Shape> = Box::new(Circle { radius: 2.0 });
println!("{}", boxed.describe());          // This shape has area 12.57

Sources

1
The Rust Project. The Rust Programming Language, "Traits: Defining Shared Behavior" and "Using Trait Objects", doc.rust-lang.org/book/ch10 & ch18.