Trait Objects and Dynamic Dispatch

Trait Objects

A trait object is an instance of dyn Trait accessed behind a pointer or reference (such as &dyn Trait, &mut dyn Trait, Box<dyn Trait>, Rc<dyn Trait>, or Arc<dyn Trait>) that pairs a pointer to a concrete value with a pointer to its implementation of Trait.

Unsized Nature of dyn Trait (!Sized)

Because different types implementing a trait Render can have completely different sizes in memory (for example, a 0-byte unit struct vs. a 64-byte struct), the type dyn Render itself has no fixed compile-time size (dyn Render: !Sized). Attempting to store a bare dyn Render directly in a local variable or pass it by value without indirection fails with E0277:

// ❌ does not compile
trait Render {
    fn render(&self) -> String;
}

fn show(item: dyn Render) {
    println!("{}", item.render());
}

fn main() {}
error[E0277]: the size for values of type `(dyn Render + 'static)` cannot be known at compilation time
 --> src/main.rs:6:15
  |
6 | fn show(item: dyn Render) {
  |               ^^^^^^^^^^ doesn't have a size known at compile time
  |
  = help: the trait `Sized` is not implemented for `(dyn Render + 'static)`
  = help: unsized fn params are gated as an unstable feature
help: function arguments must have a statically known size, borrowed types always have a known size
  |
6 | fn show(item: &dyn Render) {
  |               +

Creating Trait Objects via Unsize Coercion

You create a trait object by taking a reference or smart pointer to a concrete type (&Concrete or Box<Concrete>) and letting the compiler perform an implicit unsize coercion to &dyn Trait or Box<dyn Trait> (or using an explicit as &dyn Trait cast):

trait Codec {
    fn name(&self) -> &str;
    fn encode(&self, input: &[u8]) -> Vec<u8>;
}

struct IdentityCodec;

impl Codec for IdentityCodec {
    fn name(&self) -> &str {
        "identity"
    }

    fn encode(&self, input: &[u8]) -> Vec<u8> {
        input.to_vec()
    }
}

struct XorCodec {
    key: u8,
}

impl Codec for XorCodec {
    fn name(&self) -> &str {
        "xor"
    }

    fn encode(&self, input: &[u8]) -> Vec<u8> {
        input.iter().map(|b| b ^ self.key).collect()
    }
}

fn run_codec(codec: &dyn Codec, payload: &[u8]) {
    let encoded = codec.encode(payload);
    println!("{:<8} -> {:02X?}", codec.name(), encoded);
}

fn main() {
    let id = IdentityCodec;
    let xor = XorCodec { key: 0xFF };

    // Implicit unsize coercion from `&IdentityCodec` and `&XorCodec` to `&dyn Codec`:
    run_codec(&id, &[0x01, 0x02, 0x03]);
    run_codec(&xor, &[0x01, 0x02, 0x03]);
}
identity -> [01, 02, 03]
xor      -> [FE, FD, FC]

Downcasting Trait Objects with std::any::Any

Once a concrete type is coerced into &dyn Trait, its concrete type identity is erased. If you need to recover the original concrete type at runtime, bound your trait by the std::any::Any supertrait (trait MyTrait: Any) on 'static types and use <dyn Any>::downcast_ref::<ConcreteType>().