Returning a trait¶
Level: 201 · working knowledge
One line: A function cannot return Trait, because the caller has to know the size before the branch is taken — impl Trait fixes that when one type comes back, Box<dyn Trait> when several can.
trait Method { fn name(&self) -> &'static str; }
struct Star;
struct BlocStar;
impl Method for Star { fn name(&self) -> &'static str { "STAR" } }
impl Method for BlocStar { fn name(&self) -> &'static str { "Bloc STAR" } }
fn method_for(seats: usize) -> Box<dyn Method> {
if seats == 1 { Box::new(Star) } else { Box::new(BlocStar) }
}
fn main() {
println!("{}", method_for(3).name()); // Bloc STAR
}
Why -> Method is not allowed¶
The compiler has to know how much stack space every return value needs, and it has to know it at the call site, before the function runs. Star and BlocStar are different types of different sizes, so "returns a Method" does not answer the question — there is no single size that is right for both.
A Box answers it. The value goes on the heap, and what comes back is a pointer, whose size is fixed no matter what it points at. Rust asks you to write that heap allocation down rather than inserting it quietly, which is why the return type says Box<dyn Method> and not Method.
impl Trait when the answer is one type¶
If only one concrete type can ever come back, say so with impl Trait and nothing is boxed:
This is not "some type decided at run time" — the compiler knows perfectly well it is a Star. It is the caller who is not told, so the function keeps the freedom to change its mind later without changing its signature. The value stays on the stack, the call is dispatched statically, and there is no vtable.
impl Trait also works as an argument type, where it is shorthand for a generic parameter used exactly once:
fn count(src: impl std::io::BufRead) -> usize { src.lines().count() }
// the same thing as: fn count<R: std::io::BufRead>(src: R) -> usize
The one thing you lose is the turbofish: with impl Trait in argument position there is no type parameter to name, so count::<File>(f) will not compile.
And the case that made impl Trait necessary rather than convenient: a closure's type has no name. Before it existed, returning one meant boxing it. Now it does not.
What the two spellings actually cost¶
impl Trait |
Box<dyn Trait> |
|
|---|---|---|
| How many types can come back | exactly one | any number |
| Where the value lives | the stack | the heap |
| Dispatch | static — the call is compiled straight to the impl | dynamic — one indirection through a vtable |
| Code size | one copy per concrete type (monomorphization ↗) | one copy, shared |
A &dyn Trait is a fat pointer: two words, one to the data and one to the vtable. The verified run below prints 8 for &Star and 16 for &dyn Method, which is that second word made visible.
The verified output¶
Verified output of returning_a_trait.rs — regenerated by tools/run_examples.py, never hand-typed.
1. One function, two concrete types — chosen at run time
1 seat(s) -> STAR counts 1 seat(s)
3 seat(s) -> Bloc STAR counts 3 seat(s)
2. `impl Trait` when the answer is always the same type
single_winner_method() -> STAR
3. Why the box was needed: sizes
Star 0 bytes a unit struct holds nothing
BlocStar 8 bytes one usize
&Star 8 bytes one pointer; the type is known
&dyn Method 16 bytes TWO pointers: the data, and the vtable
Box<dyn Method> 16 bytes same pair, owned
4. The vtable is what makes the call land in the right impl
STAR -> 1 seat(s)
Bloc STAR -> 5 seat(s)
See also¶
- Static vs dynamic dispatch — the same two spellings in argument position, where it is a design choice rather than a compiler requirement, plus what makes a trait usable as
dynat all - What a trait is — the declaration being returned
- Nullable pointers —
Boxin its other role, making a recursive type possible Stringvs&str— the same owned/borrowed split, one layer down
Po polsku¶
Nawyk z Javy albo C# jest tu dokładnie odwrotny do tego, czego wymaga Rust: tam zwrócenie interfejsu jest rzeczą zupełnie normalną, bo każdy obiekt i tak jest referencją o stałym rozmiarze. W Ruscie -> Method nie przechodzi, ponieważ miejsce wywołania musi znać rozmiar zwracanej wartości, zanim funkcja się w ogóle wykona — a Star i BlocStar mają różne rozmiary, więc „zwraca Method” nie odpowiada na to pytanie. Box<dyn Method> odpowiada: wartość ląduje na stercie, a wraca wskaźnik, który zawsze waży tyle samo. Rust każe tę alokację zapisać jawnie, zamiast wstawiać ją po cichu za plecami programisty.
Drugie zaklęcie, impl Trait, bywa streszczane jako „jakiś typ wybierany w czasie działania” — i to jest nieporozumienie warte zapamiętania. Kompilator doskonale wie, że wraca Star; nie wie tego wywołujący. Funkcja zachowuje przez to swobodę zmiany zdania bez ruszania sygnatury, wartość zostaje na stosie, wywołanie jest rozstrzygane statycznie i żadnej tablicy metod wirtualnych nie ma. W pozycji argumentu impl Trait to po prostu typ generyczny użyty dokładnie raz — a płaci się za to turbofishem: nie ma parametru typu do nazwania, więc count::<File>(f) się nie skompiluje.
Jest jednak przypadek, w którym impl Trait nie jest wygodą, tylko koniecznością: domknięcie (closure) nie ma nazwy typu, więc zanim ten zapis powstał, zwrócenie domknięcia oznaczało opakowanie go w Box. Warto też zapamiętać liczby z wydruku powyżej: &Star waży 8 bajtów, a &dyn Method szesnaście, bo referencja do obiektu cechy (trait object) jest grubym wskaźnikiem — jedno słowo maszynowe na dane, drugie na tablicę metod wirtualnych. To drugie słowo widać na wydruku i to ono jest całą ceną dynamicznego wywołania.
Szukaj po polsku: obiekt cechy · gruby wskaźnik · tablica metod wirtualnych · rust impl Trait vs Box dyn Trait · rust return trait object · rust Sized return value