A trait is a promise about behavior
In the previous guide you watched the borrow checker reason about who owns what and for how long. Traits answer a different question: *what can a type do?* A trait is a named bundle of method signatures — a promise that any type implementing it provides those methods. If you have written C, the closest thing you know is a struct full of function pointers that you fill in by hand, or a header that declares functions a `.c` file must define. A trait is that idea made first-class and checked by the compiler. You declare `trait Draw { fn draw(&self); }`, and then for each concrete type you write `impl Draw for Circle { fn draw(&self) { ... } }`. Now `Circle` is a `Draw` — not by inheritance, but by having kept the promise.
The reason traits matter so much in Rust is that you almost never name a concrete type when you want flexibility — you name a trait. A function that says it takes any `T` that implements `Draw` works for `Circle`, `Square`, and a type someone writes next year, without changing. That phrase "any `T` that implements `Draw`" is a trait bound, and it is the single most common shape in real Rust code. You will see it written `fn render<T: Draw>(shape: &T)`, which reads as: render works for every type `T`, provided `T` keeps the `Draw` promise.
Static dispatch: the compiler bakes in the call
When you write `fn render<T: Draw>(shape: &T)`, something quietly aggressive happens at compile time. The compiler does not produce one `render` that figures out at runtime which `draw` to call. Instead, for every concrete type you actually call it with, it stamps out a separate, specialized copy: a `render` for `Circle` with `Circle::draw` wired directly in, a `render` for `Square` with `Square::draw` wired directly in. This stamping-out is monomorphization — "making it single-typed" — and the next guide is devoted to it. The effect here is static dispatch: the exact function to call is decided at compile time and written straight into the machine code.
The payoff is speed, and not in a vague way. A statically dispatched call compiles to an ordinary direct call instruction — the same thing C generates for a plain function call. Because the target is known, the optimizer can go further and inline the body, erasing the call entirely and then optimizing across the boundary as if you had written one fused function by hand. This is the foundation of Rust's "zero-cost abstraction" promise: the trait gave you the flexibility of writing `render` once, and after compilation there is no trace of that flexibility left in the binary — no indirection, no lookup, nothing to pay for at runtime.
There is a cost, but it lands at compile time and in binary size, not at runtime. If you call `render` with ten different types, you get ten copies of `render` in the executable. That is the deal monomorphization strikes: it trades a bigger binary and slower compiles for the fastest possible call. For most code this is exactly the trade you want, which is why generic-with-trait-bounds is the default tool you reach for first.
Trait objects: one type, a hidden table of functions
Static dispatch needs to know every concrete type at compile time. But sometimes you genuinely cannot: you want a single `Vec` holding a circle, a square, and a text box all mixed together, and you want to loop over it calling `draw` on each. A `Vec<T>` is monomorphized to one element type — it cannot hold a mix. The escape hatch is a trait object, written `&dyn Draw` or `Box<dyn Draw>`. The keyword `dyn` is a deliberate flag that says: *do not specialize this; dispatch at runtime.* Now `Vec<Box<dyn Draw>>` happily holds any mix of shapes, because every element has the same static type — "some `Draw`, decided later."
To make that work, a trait object is not a plain pointer — it is a fat pointer, two machine words side by side. The first word points at the data (the actual `Circle` sitting on the heap or stack); the second points at a vtable, a small static table of function pointers, one slot per method in the trait, filled with that concrete type's implementations. Calling `obj.draw()` on a `&dyn Draw` means: load the vtable pointer, index to the `draw` slot, load the function pointer there, and call through it. This runtime lookup is dynamic dispatch, and the vtable is exactly the hand-rolled struct-of-function-pointers a C programmer builds for polymorphism — except the compiler builds and wires it for you, correctly, every time.
&dyn Draw is two words:
+-----------+ +----------------------------+
| data ptr | -----> | the Circle's bytes |
+-----------+ +----------------------------+
| vtable ptr| --+
+-----------+ | vtable for Circle as Draw:
+--> +----------------------------+
| drop_in_place(Circle) ptr |
| size = 8, align = 8 |
| draw -> Circle::draw ptr | <- method slot
+----------------------------+
obj.draw() = call (*(obj.vtable + draw_slot))(obj.data)Choosing between them, honestly
So which do you write? The honest answer is that neither is universally better; they trade different things, and the trade is real. Static dispatch is faster per call (a direct call, often inlined) but inflates the binary and the compile time, and it cannot store mixed types in one collection. Dynamic dispatch keeps the binary small and lets you mix types freely, but every call pays for the indirection — a pointer load and a call through it — and the optimizer usually cannot inline across it, so you also lose the optimizations inlining would have unlocked. Do not believe anyone who tells you the vtable call is "basically free" or, conversely, that it is "slow": it is one extra indirect call, frequently invisible, occasionally the hot spot. Measure when it matters.
- Reach for static dispatch (`fn f<T: Draw>`) by default: it is the fastest call and keeps full optimization across the boundary.
- Switch to a trait object (`Box<dyn Draw>`) when you must store a heterogeneous collection — a `Vec` of genuinely different types behind one trait.
- Prefer `dyn` too when monomorphization would explode: a generic instantiated over very many types can bloat the binary badly, and one shared `dyn` copy is leaner.
- Remember the requirement: a trait must be object-safe to be used as `dyn`. Roughly, its methods must be callable through a pointer — no generic methods, and methods must take `self` by reference, not by value-of-`Self`.
The pieces that make traits powerful
A trait can carry more than methods. An associated type lets a trait name a type it works with but leave the choice to the implementer. The classic case is `Iterator`: the trait declares `type Item;` and `fn next(&mut self) -> Option<Self::Item>;`, and when you `impl Iterator for Counter` you pin `type Item = u32`. The caller writes `Self::Item` and gets whatever the implementer chose, with no extra type parameter cluttering every signature. This is why iterating in Rust feels so natural and yet compiles down to a tight loop with no boxing.
Two more pieces shape how traits compose across a whole program. A blanket implementation is an `impl` written for all types that satisfy some bound at once — `impl<T: Display> ToString for T` gives every displayable type a `to_string()` in one stroke, no per-type work. And the orphan rule is the guardrail that keeps this sane: you may implement a trait for a type only if you own the trait or you own the type. Without it, two unrelated crates could each implement the same foreign trait for the same foreign type, and the compiler would have no principled way to pick. The rule can feel restrictive, but it is what lets the compiler guarantee there is exactly one implementation to dispatch to — the same guarantee that makes both static and dynamic dispatch unambiguous.
One last thread back to ownership, because it is everywhere once you see it. The closures you pass around — `|x| x + 1` and friends — are values whose types implement the `Fn`, `FnMut`, or `FnOnce` traits, so closures are just traits in disguise, and the same static-versus-dynamic choice applies: `impl Fn(i32) -> i32` is monomorphized and inlined, while `Box<dyn Fn(i32) -> i32>` is a fat pointer with a vtable. Even cleanup follows this pattern: the `Drop` trait you met cleaning up resources is dispatched through the vtable for a `dyn` value, which is why a `Box<dyn Draw>` still runs the right destructor when it falls out of scope. Traits are not one feature among many in Rust — they are the connective tissue, and the dispatch question runs through all of it.