Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to choose behaviour by type

Generics, impl Trait, dyn Trait and enums. Four ways for one piece of code to work with several types, and what each costs.

Ask: is the list of types closed, and do they all have to live in one collection?

  • You know every case, and you own them all (the shapes in your drawing app, the commands your tool accepts) → an enum and a match. The compiler tells you about every place a new variant has to be handled.
  • Any type with the right trait, and each call uses one type → a generic with a trait bound (fn log<T: Display>(x: T)), or impl Trait in argument position, which is the same thing written shorter. Fastest option: a copy is compiled per type.
  • You return one concrete type and don't want to spell it out (an iterator chain, a closure) → impl Trait in return position. It must be the same type on every path.
  • Different types in one Vec, or chosen at run time, or added by code you haven't seen (plugins, a user's own handlers) → Box<dyn Trait> or &dyn Trait. One compiled copy, one indirect call.

A common path is to start with an enum, switch to a generic when other people's types have to fit, and use dyn only where the types really are mixed at run time.

A closed set of cases

One type, a fixed list of shapes, each carrying its own data.

Formal. A match expression must have an arm for every value its scrutinee's type can hold; a missing case is error E0004, which names a value that isn't covered. The wildcard pattern _ matches anything.

In plain English. When you match, the compiler checks that you handled every possibility, and tells you exactly which one you forgot.

Example. match n { 0 => "zero", 1..=9 => "small" } on an i32 is E0004 (negative numbers and 10 upward aren't covered). Adding _ => "other" fixes it.

Why this rule exists. Forgetting a case is one of the most common bugs in branching code. Making it a compile error means adding a new case later shows you every place that must handle it.

Why adding a variant is safe. Every match that misses it stops compiling.

Any type with the trait, chosen at compile time

One definition, any T, with the bounds the body needs.

How a generic says what it needs from T.

Formal. For each distinct set of concrete type (and const) arguments a generic function or type is used with, the compiler generates a separate specialized copy. Calls to it are resolved at compile time (static dispatch) and may be inlined, so generic code runs as fast as code written by hand for each type, at the cost of compile time and binary size.

In plain English. Writing a function once for "any type T" doesn't make the program look up anything while it runs. The compiler quietly writes one version for i32, another for f64, and so on, as if you had typed them all.

Example. Calling count_above(&[1, 2, 3], &2) and count_above(&[1.5, 0.5], &1.0) produces two machine-code functions, count_above::<i32> and count_above::<f64>.

Why this rule exists. It gives Rust abstractions without a runtime price, which is what lets high-level code be used where speed matters.

The compiler writes a copy per type, so the calls are direct and inlinable.

Choosing which function to call at compile time.

Example

describe(3) and describe("hi") on a generic fn describe<T: Display>(x: T) call two separate compiled functions.

One hidden type

Return an iterator chain or closure without naming its type.

Formal. A function declared -> impl Trait returns values of exactly one concrete type, chosen by the function body and hidden from callers, who may use only Trait (and auto traits such as Send that the hidden type has). Returning different concrete types from different branches is a type mismatch (E0308). In edition 2024 the hidden type may capture every generic parameter and lifetime in scope unless a use<..> bound lists fewer.

In plain English. "I'll give you something that can do X. I won't tell you what it is, but it's always the same kind of thing." If you need to hand back different kinds of things, you need a box that can hold any of them.

Example. fn evens() -> impl Iterator<Item = u32> { (0..).step_by(2) } compiles; an if returning (0..3) in one branch and vec![1].into_iter() in the other does not.

Why this rule exists. One concrete type keeps the call statically dispatched and allocation-free, while still letting the function change its internal type without breaking callers.

Why if x { a.iter() } else { b.into_iter() } won't compile behind one impl Trait.

Any type, chosen at run time

Section

dyn Trait

Mixed types in one collection, behind a pointer.

What a &dyn Trait holds: the data pointer and a table of methods.

Formal. A trait can be used as dyn Trait only if it is dyn compatible: it has no Sized supertrait, no associated constants, and every method that isn't marked where Self: Sized takes a receiver (&self, &mut self, Box<Self>, ...), has no type parameters, and does not use Self except as that receiver. Using an incompatible trait as dyn Trait is error E0038.

In plain English. A trait object only knows "some type that has these methods," reached through a table of function pointers. A method that needs to know the real type (to return one, or to be copied out for every possible generic type) can't be put in that table.

Example. Clone isn't dyn compatible, because clone returns Self; Display is, because fmt(&self, ..) only borrows.

Why this rule exists. Every method of a trait object must be callable through its vtable without knowing the concrete type.

Which traits can be used as dyn at all (no generic methods, no Self by value).

Choosing

The costs side by side, measured.

One program written all three ways, then changed to see which bends.

impl Trait or dyn Trait for the dependency you swap out in tests.