Question

Rust Trait Objects in Struct Fields: Box, References, Lifetimes, and self

rusttraits

Question

Given this Rust trait:

pub trait Foo {
    fn do_something(&self) -> f64;
}

how can a Bar struct store a value that implements Foo?

pub struct Bar {
    foo: Foo,
}

This fails because a trait is used where Rust expects a concrete type. The compiler suggests using Box<Foo> or &Foo. Why does a struct field such as foo: &Foo require a lifetime specifier, while foo: Box<Foo> does not? Why can I not move a Box<Foo> out of &self in a method, and what does it mean when a getter takes self rather than &self?

Using current Rust syntax, the working version is:

trait Foo {
    fn do_something(&self) -> f64;
}

struct Bar {
    foo: Box<dyn Foo>,
}

impl Bar {
    fn get_foo(self) -> Box<dyn Foo> {
        self.foo
    }
}

Short Answer

You will learn why traits need indirection when stored in structs, when to use Box<dyn Trait> versus &dyn Trait, how lifetimes describe borrowed fields, and why a method taking self can move a field while one taking &self cannot.

Concept

A trait such as Foo describes behavior, not one specific concrete memory layout. Many types can implement it, and those types may have different sizes:

struct Number;
struct Decimal(f64);

impl Foo for Number {
    fn do_something(&self) -> f64 { 1.0 }
}

impl Foo for Decimal {
    fn do_something(&self) -> f64 { self.0 }
}

Because Rust does not know the size of an arbitrary Foo implementation, dyn Foo is a dynamically sized type. A dynamically sized value cannot be stored directly as an ordinary struct field.

Instead, store a pointer-like value with a known size:

  • Box<dyn Foo>: Bar owns the implementation, allocated on the heap.
  • &'a dyn Foo: Bar borrows an implementation owned elsewhere.
  • Arc<dyn Foo>: Bar shares ownership, often across threads when the trait object meets thread-safety bounds.

In modern Rust, write dyn Foo for a trait object. Older Rust code may show Box<Foo> or &Foo, but Box<dyn Foo> makes the dynamic dispatch explicit.

A trait object pointer contains enough information to call trait methods on an unknown concrete type. Conceptually, it carries a pointer to the value plus information about how to use it as Foo.

If you do not need runtime polymorphism, a generic type parameter is often another option:

struct GenericBar<T: Foo> {
    foo: T,
}

This stores T directly and is usually compiled using static dispatch, but each T creates a distinct GenericBar<T> type.

Mental Model

Think of Foo as an interface for appliances, not an appliance itself.

  • A struct field needs to know how much shelf space it occupies.
  • dyn Foo could be a toaster, a refrigerator, or another appliance with a different size, so it cannot occupy a fixed-size shelf slot directly.
  • Box<dyn Foo> is a fixed-size label containing the location of an appliance that Bar owns.
  • &'a dyn Foo is a fixed-size borrowed-address card: it points to an appliance owned by someone else. The lifetime 'a is the return deadline on that card: Bar must not keep it after the owner takes the appliance away.

For method receivers:

  • &self means “borrow the box; do not take anything from it.”
  • self means “hand over the entire Bar; its fields may now be moved out.”
Take Quiz

Syntax and Examples

Use Box<dyn Foo> when the struct should own any value implementing the trait.

trait Foo {
    fn do_something(&self) -> f64;
}

struct Temperature(f64);

impl Foo for Temperature {
    fn do_something(&self) -> f64 {
        self.0
    }
}

struct Bar {
    foo: Box<dyn Foo>,
}

impl Bar {
    fn value(&self) -> f64 {
        self.foo.do_something()
    }

    fn into_foo(self) -> Box<dyn Foo> {
        self.foo
    }
}

fn main() {
    let bar = Bar {
        foo: Box::new(Temperature(21.5)),
    };

    println!("{}", bar.value());

    let foo = bar.into_foo();
    println!("{}", foo.do_something());
}

value(&self) only reads through the box, so it borrows Bar.

into_foo(self) consumes Bar, allowing self.foo to move out. The name into_foo follows a common Rust convention: methods starting with into_ consume their receiver and convert it into another value.

Use a reference when another value owns the implementation:

trait Foo {
    fn do_something(&self) -> f64;
}

struct Bar<'a> {
    foo: &'a dyn Foo,
}

impl<'a> Bar<'a> {
    fn value(&self) -> f64 {
        self.foo.do_something()
    }
}

The lifetime 'a says that the borrowed Foo must remain valid for at least as long as the Bar<'a> that points to it.

Step by Step Execution

Consider this owned trait-object example:

trait Foo {
    fn do_something(&self) -> f64;
}

struct Fixed;

impl Foo for Fixed {
    fn do_something(&self) -> f64 {
        42.0
    }
}

struct Bar {
    foo: Box<dyn Foo>,
}

impl Bar {
    fn read(&self) -> f64 {
        self.foo.do_something()
    }

    fn into_foo(self) -> Box<dyn Foo> {
        self.foo
    }
}

fn main() {
    let bar = Bar { foo: Box::new(Fixed) };
    let answer = bar.read();
    let foo = bar.into_foo();

    println!("{answer}, {}", foo.do_something());
}

Execution and ownership flow:

  1. Box::new(Fixed) allocates a Fixed value and produces a Box<Fixed>.
  2. Rust coerces Box<Fixed> to Box<dyn Foo> because Fixed implements Foo.
  3. bar.read() automatically borrows bar as &Bar. The method reads foo through a shared borrow and returns 42.0.
  4. bar is still usable after read, because nothing was moved out.
  5. bar.into_foo() passes ownership of bar into the method.
  6. The method moves the foo field out and returns it. bar no longer exists afterward.
  7. The returned box still owns Fixed, so calling do_something() remains valid.

This would be rejected:

impl Bar {
    fn get_foo(&self) -> Box<dyn Foo> {
        self.foo
    }
}

self is only a shared borrow (&Bar) here. Moving the Box would leave the original Bar with a missing field, which is not allowed through a shared borrow.

Real World Use Cases

Trait-object fields are useful when a component must work with several implementations chosen at runtime.

  • Pluggable storage: Store Box<dyn Storage> so an application can use an in-memory store locally and a database-backed store in production.
  • Payment providers: Store Box<dyn PaymentProcessor> to select Stripe-like, test, or regional implementations from configuration.
  • UI components: Keep heterogeneous widgets in Vec<Box<dyn Widget>> when each widget implements the same drawing and event-handling trait.
  • Command handlers: Store different commands behind Box<dyn Command> and execute each through a common method.
  • Testing: Pass &dyn Clock or Box<dyn Clock> so production code uses a system clock while tests use a deterministic fake clock.

Choose a borrowed trait object when the dependency is created and owned by a higher-level object. Choose a boxed trait object when the containing struct should own the dependency independently.

Take Quiz

Real Codebase Usage

In real Rust projects, trait objects are usually hidden behind constructors and accessed through behavior-focused methods rather than returned directly.

Constructor injection

trait Logger {
    fn log(&self, message: &str);
}

struct Service {
    logger: Box<dyn Logger>,
}

impl Service {
    fn new(logger: Box<dyn Logger>) -> Self {
        Self { logger }
    }

    fn run(&self) {
        self.logger.log("service started");
    }
}

This makes the dependency explicit and makes testing straightforward.

Prefer operations over exposing fields

Instead of returning the whole trait object, provide the operation callers need:

impl Bar {
    fn result(&self) -> f64 {
        self.foo.do_something()
    }
}

This keeps Bar in control of its internal representation.

Borrow rather than transfer ownership

If a caller only needs to call trait methods, return a borrowed trait object:

impl Bar {
    fn foo(&self) -> &dyn Foo {
        self.foo.as_ref()
    }
}

This does not move the Box; it returns a reference tied to the borrow of Bar.

Explicit ownership transfer

When a caller must take ownership of the implementation, use a consuming method and name it accordingly:

impl Bar {
    fn into_foo(self) -> Box<dyn Foo> {
        self.foo
    }
}

A struct with Box<dyn Trait> is not automatically Clone. If code needs to duplicate it, the trait and design must explicitly support cloning, or the project may use shared ownership such as Arc.

Common Mistakes

Omitting dyn in current Rust

Older examples may use Box<Foo>. In current Rust, write:

Box<dyn Foo>

The dyn keyword makes it clear that calls use dynamic dispatch.

Storing a trait directly

This does not work:

struct Bar {
    foo: dyn Foo,
}

dyn Foo has no compile-time-known size. Use a generic parameter or an indirection such as Box<dyn Foo>.

Forgetting the lifetime on a reference field

This does not compile:

struct Bar {
    foo: &dyn Foo,
}

A field reference needs a lifetime because the struct may live beyond the scope where it was created. Correct version:

struct Bar<'a> {
    foo: &'a dyn Foo,
}

Returning an owned box from &self

This tries to move out of a borrowed value:

impl Bar {
    fn get_foo(&self) -> Box<dyn Foo> {
        self.foo
    }
}

Use one of these based on intent:

fn foo(&self) -> &dyn Foo {
    self.foo.as_ref()
}

fn into_foo(self) -> Box<dyn Foo> {
    self.foo
}

Expecting a value to remain usable after a self method

let bar = Bar { /* ... */ };
let foo = bar.into_foo();
// bar.read(); // Error: `bar` was moved.

A method taking self consumes the value unless its type implements Copy, which a Bar containing Box<dyn Foo> cannot do.

Using a trait that is not object-safe

Not every trait can become dyn Trait. For example, a method returning Self prevents ordinary trait-object use:

trait BadTrait {
    fn make_another(&self) -> Self;
}

A trait object needs methods whose calls can be described without knowing the concrete implementing type. Keep traits intended for dyn Trait focused on object-safe methods.

Comparisons

ApproachOwnershipLifetime needed on BarBest whenTrade-off
Box<dyn Foo>Bar owns the valueNoImplementation is selected at runtime and should belong to BarHeap allocation and dynamic dispatch
&'a dyn FooAnother value owns itYesBar only needs temporary access to an existing dependencyOwner must outlive Bar
T: Foo / Bar<T>Bar owns T directlyNoConcrete type is known at compile timeOne Bar<T> type per implementation; less flexible runtime selection
Arc<dyn Foo>Shared ownershipNoSeveral owners need the same implementationReference-counting overhead; threads may require Send + Sync

&self versus self

ReceiverMeaningCan move self.foo out?Typical use
&selfShared borrowNoRead data or call non-mutating methods
&mut selfExclusive mutable borrowUsually no for ordinary fieldsUpdate fields in place
selfTakes ownershipYesConsume, transform, or dismantle the value

A generic field and a trait-object field solve related but different problems:

struct StaticBar<T: Foo> {
    foo: T,
}

struct DynamicBar {
    foo: Box<dyn Foo>,
}

Use StaticBar<T> when the implementation can be chosen at compile time. Use DynamicBar when one variable may hold different implementations at runtime.

Take Quiz

Cheat Sheet

trait Foo {
    fn do_something(&self) -> f64;
}

// Own an unknown implementation.
struct OwnedBar {
    foo: Box<dyn Foo>,
}

// Borrow an implementation owned elsewhere.
struct BorrowedBar<'a> {
    foo: &'a dyn Foo,
}

// Store a known-at-compile-time implementation directly.
struct GenericBar<T: Foo> {
    foo: T,
}
impl OwnedBar {
    // Read without consuming `self`.
    fn value(&self) -> f64 {
        self.foo.do_something()
    }

    // Borrow the contained trait object.
    fn foo(&self) -> &dyn Foo {
        self.foo.as_ref()
    }

    // Transfer ownership of the box.
    fn into_foo(self) -> Box<dyn Foo> {
        self.foo
    }
}
  • Prefer dyn Foo over legacy bare trait-object syntax.
  • A dyn Trait value needs indirection because its size is not known at compile time.
  • Reference fields require explicit lifetimes, such as Bar<'a>.
  • &self borrows; it cannot move a non-Copy field out.
  • self consumes the struct, so fields can be moved out.
  • Use an into_... method name for ownership-consuming conversions.
  • Trait objects require object-safe traits.

FAQ

Why does &dyn Foo need a lifetime in a struct?

A reference is borrowed data. Rust needs to know how long that borrow remains valid so that Bar cannot outlive the value it references. Write struct Bar<'a> { foo: &'a dyn Foo }.

Why does Box<dyn Foo> not need a lifetime parameter?

Box owns its allocation. The boxed value is dropped with the box, so no external owner needs to outlive Bar.

Can I return &dyn Foo from a struct containing Box<dyn Foo>?

Yes. This borrows the value instead of moving it:

fn foo(&self) -> &dyn Foo {
    self.foo.as_ref()
}

Why can a method taking self return self.foo?

The method owns the entire Bar. It is therefore allowed to move the foo field into the return value. The original Bar is consumed.

Should I always use Box<dyn Trait> for traits in structs?

No. Use a generic T: Trait when the concrete type is known at compile time. Use Box<dyn Trait> when you need runtime polymorphism or heterogeneous implementations behind one field type.

Does Box<dyn Foo> copy the value when returned?

No. Returning it moves the Box, which transfers ownership of the allocation. The underlying value is not duplicated.

What does dyn mean in dyn Foo?

It indicates dynamic dispatch: Rust selects the appropriate trait-method implementation at runtime based on the concrete value stored behind the trait object.

Can every trait be used as dyn Trait?

No. The trait must be object-safe. In particular, methods that require knowing the concrete Self type can make a trait unsuitable for trait objects.

Take Quiz

Mini Project

Description

Build a small report service that can use different score sources through one common trait. It demonstrates how a struct owns a runtime-selected implementation with Box<dyn ScoreSource>, how to call it through &self, and how to transfer it out with a consuming method.

Goal

Create a Report that prints a score from any type implementing ScoreSource and can later return ownership of that source.

Requirements

Create a ScoreSource trait with a method returning an f64 score.|Implement the trait for at least two different source types.|Store a Box<dyn ScoreSource> inside a Report struct.|Add a borrowed method that creates a report message without consuming the Report.|Add an into_source method that transfers the boxed source to the caller.

Take Quiz

Keep learning

Related questions

View more