динамическая диспетчеризацияdynamic dispatch
dyn Trait — стёртый по типу, диспетчеризуемый во время выполнения указатель на "что угодно, реализующее этот трейт", используемый только за указателем (&dyn Trait, Box<dyn Trait>, Rc<dyn Trait>). В отличие от мономорфизированного дженерика, это единственная скомпилированная форма — "толстый указатель" из указателя на данные и указателя на vtable, разрешаемая во время выполнения ровно как виртуальный вызов в C++.dyn Trait is a type-erased, runtime-dispatched pointer to "anything implementing this trait", used only behind a pointer (&dyn Trait, Box<dyn Trait>, Rc<dyn Trait>). Unlike a monomorphized generic, it is a single compiled form — a "fat pointer" made of a data pointer plus a vtable pointer, resolved at runtime exactly like a C++ virtual call.
Trait object (dyn Trait) — способ хранить и вызывать значения любого конкретного типа, реализующего трейт, через один и тот же скомпилированный путь, известный только во время выполнения. Физически &dyn Trait — это "толстый указатель" из двух слов: указатель на сами данные и указатель на vtable — таблицу указателей на функции, реализующие методы трейта для этого конкретного типа. Это ровно то же самое, что происходит в C++ при вызове виртуальной функции через указатель на базовый класс: объект хранит указатель на vtable, а вызов идёт через неё.A trait object (dyn Trait) is a way to store and call values of any concrete type implementing a trait through the same compiled path, known only at runtime. Physically, &dyn Trait is a two-word "fat pointer": a pointer to the actual data and a pointer to a vtable — a table of function pointers implementing the trait's methods for that concrete type. This is exactly what happens in C++ when calling a virtual function through a base-class pointer: the object carries a vtable pointer, and the call goes through it.
fn print_area<T: Shape>(shape: &T) {
println!("{}", shape.area());
} // компилятор генерирует отдельную копию на каждый T// the compiler generates a separate copy per T
fn print_area(shape: &dyn Shape) {
println!("{}", shape.area());
} // одна скомпилированная функция, вызов area() через vtable// one compiled function, area() is called through the vtable
Заметка про edition: до Rust 2018 ключевое слово dyn было необязательным — писали просто Box<Shape>/&Shape, и было легко случайно спутать trait object с обычным generic-ограничением при беглом чтении кода. Начиная с edition 2018 (сам тип поведения не изменился — это чисто синтаксическое требование, применяется даже к более старому коду при явном указании edition = "2018" и новее) явное dyn перед именем трейта стало обязательным именно для того, чтобы динамическая диспетчеризация была видна в сигнатуре, а не угадывалась по контексту.Edition note: before Rust 2018, the dyn keyword was optional — code simply wrote Box<Shape>/&Shape, and it was easy to mistake a trait object for an ordinary generic bound on a quick read. Starting with edition 2018 (the underlying behavior didn't change — this is a purely syntactic requirement, applied even to older code once it opts into edition = "2018" or newer), an explicit dyn before the trait name became mandatory specifically so dynamic dispatch is visible in the signature rather than inferred from context.
Мономорфизированные дженерики (см. traits.html) не могут выразить одну задачу: однородную коллекцию, хранящую РАЗНЫЕ конкретные типы, реализующие один трейт.Monomorphized generics (see traits.html) cannot express one particular task: a single homogeneous collection holding DIFFERENT concrete types that implement the same trait.
Vec<T> с ограничением T: Shape после мономорфизации превращается в Vec<Circle> ИЛИ Vec<Square> — но никогда в смесь того и другого одновременно. Компилятор фиксирует конкретный T на этапе компиляции для каждого использования — гетерогенность в принципе недостижима через этот механизм.A Vec<T> bounded by T: Shape becomes, after monomorphization, either Vec<Circle> OR Vec<Square> — never a mix of both at once. The compiler fixes a concrete T at compile time for each use — heterogeneity is fundamentally unreachable through this mechanism.
Vec<Box<dyn Shape>> может хранить действительно разнородный набор — круги и квадраты вперемешку, с диспетчеризацией конкретного метода .area() отдельно для каждого элемента во время выполнения.Vec<Box<dyn Shape>> can hold a genuinely mixed set — circles and squares intermixed, with the concrete .area() method dispatched separately for each element at runtime.
Это ровно та же задача "рантайм-полиморфизм над гетерогенной коллекцией", которую в ООП-языках решают абстрактные базовые классы и виртуальные функции — Rust решает её без наследования классов, через трейты и явное указание dyn в точке использования.This is exactly the "runtime polymorphism over a heterogeneous collection" problem that abstract base classes and virtual functions solve in OOP languages — Rust solves it without class inheritance, via traits and an explicit dyn at the point of use.
Определим трейт Shape и реализуем его для двух разных типов, затем соберём их в один вектор через Box<dyn Shape>:Define a Shape trait and implement it for two different types, then collect them into one vector via Box<dyn Shape>:
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
}
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Square { side: 3.0 }),
]; // один вектор, два разных конкретных типа// one vector, two different concrete types
Физически каждый элемент Box<dyn Shape> — это два слова: указатель на данные (кучу с Circle или Square) и указатель на vtable — статическую таблицу, где для КОНКРЕТНОГО типа лежит адрес его реализации area(). Это ровно та же физическая раскладка, что у C++-объекта с одним vtable-указателем плюс собственными данными. &dyn Trait — невладеющая форма (заимствование чужих данных как trait object), Box<dyn Trait> — владеющая (данные живут на куче, освобождаются при Drop).Physically each Box<dyn Shape> element is two words: a data pointer (heap memory holding a Circle or Square) and a vtable pointer — a static table holding, for that SPECIFIC type, the address of its area() implementation. This is the exact same physical layout as a C++ object with one vtable pointer plus its own data. &dyn Trait is the non-owning form (borrowing someone else's data as a trait object), Box<dyn Trait> is the owning one (data lives on the heap, freed on Drop).
Классический случай: коллекция разных конкретных типов, объединённых общим трейтом, обходится единым циклом — каждый вызов .area() диспетчеризуется на конкретную реализацию во время выполнения.The classic case: a collection of different concrete types unified by a shared trait, walked with a single loop — each .area() call is dispatched to its concrete implementation at runtime.
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
Box::new(Circle { radius: 3.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();
println!("{}", total); // сумма площадей по разным конкретным типам// total area across different concrete types
Замыкания (см. closures.html) с разным захваченным окружением — это разные конкретные типы; Vec<Box<dyn Fn(&Event)>> позволяет хранить их вперемешку как единый список обработчиков.Closures (see closures.html) with different captured environments are different concrete types; Vec<Box<dyn Fn(&Event)>> lets you store them intermixed as a single list of handlers.
struct Event { name: String }
let mut handlers: Vec<Box<dyn Fn(&Event)>> = Vec::new();
handlers.push(Box::new(|e: &Event| println!("logger: {}", e.name)));
handlers.push(Box::new(|e: &Event| println!("metrics: {}", e.name)));
let ev = Event { name: String::from("click") };
for h in &handlers {
h(&ev); // каждый обработчик — свой конкретный тип замыкания// each handler is its own concrete closure type
}
Если крупная функция вызывается с множеством разных конкретных типов, generic-версия породит по копии на каждый тип (раздувание бинарника). &dyn Trait компилируется один раз — размен компиляции/размера бинарника на диспетчеризацию через vtable делается здесь осознанно.If a large function is called with many different concrete types, a generic version produces a copy per type (binary bloat). &dyn Trait compiles once — the compile-time/binary-size vs. vtable-dispatch tradeoff is made deliberately here.
// generic: N вызовов с N разными T -> N скомпилированных копий этой большой функции// generic: N call sites with N different T -> N compiled copies of this large function
fn render_generic<T: Shape>(shape: &T) { /* много кода *//* a lot of code */ }
// &dyn Trait: одна копия независимо от числа вызывающих конкретных типов// &dyn Trait: one copy regardless of how many concrete call-site types exist
fn render_dyn(shape: &dyn Shape) { /* тот же большой код, но скомпилирован единожды *//* the same large code, but compiled once */ }
Метод, принимающий или возвращающий Self по значению, делает трейт НЕ object-safe — vtable не может знать конкретный размер Self в точке вызова, поэтому передача/возврат по значению через типостёртый указатель невыразимы.A method taking or returning Self by value makes a trait NOT object-safe — the vtable can't know the concrete size of Self at the call site, so passing/returning by value through a type-erased pointer isn't representable.
trait NotObjectSafe {
fn combine(self, other: Self) -> Self; // Self по значению — нельзя использовать как dyn NotObjectSafe// Self by value — cannot be used as dyn NotObjectSafe
}
trait ObjectSafe {
fn area(&self) -> f64; // &self, конкретный возврат размера — можно использовать как dyn ObjectSafe// &self, a return type of known size — safe to use as dyn ObjectSafe
}
Трейт с обобщёнными методами, методами, принимающими/возвращающими Self по значению, или ассоциированными константами обычно нельзя использовать как dyn Trait вовсе — ошибка компиляции "trait cannot be made into an object". Это реальное проектное ограничение, которое нужно учитывать при разработке трейтов, изначально предназначенных для динамической диспетчеризации.A trait with generic methods, methods taking/returning Self by value, or associated constants generally cannot be used as dyn Trait at all — a compile error: "trait cannot be made into an object". This is a real design constraint to consider when defining traits intended for dynamic dispatch from the start.
Каждый вызов метода на dyn Trait проходит через косвенность vtable во время выполнения — компилятор не может его инлайнить так, как обычно инлайнит мономорфизированный generic-вызов. Для горячего кода, вызываемого миллионы раз, это реальная (хоть обычно и небольшая) цена — ровно тот же размен, что и у виртуальных функций в C++.Every method call on a dyn Trait goes through a vtable indirection at runtime — the compiler cannot inline it the way it routinely inlines a monomorphized generic call. For hot-path code called millions of times, this is a genuine (if usually small) cost — mirroring the identical tradeoff for C++ virtual functions.
dyn Trait сам по себе — unsized-тип: его размер неизвестен на этапе компиляции, ведь разные реализующие типы имеют разный размер. Использовать его можно ТОЛЬКО за каким-либо указателем (&dyn Trait, Box<dyn Trait>, Rc<dyn Trait>, Arc<dyn Trait>) — никогда как голую локальную переменную или поле структуры без косвенности. Типичная ранняя ошибка компиляции: "the size for values of type `dyn Trait` cannot be known at compilation time".dyn Trait by itself is an unsized type — its size isn't known at compile time, since different implementing types have different sizes. It can ONLY be used behind some pointer (&dyn Trait, Box<dyn Trait>, Rc<dyn Trait>, Arc<dyn Trait>) — never as a bare local variable or struct field without indirection. A common early compile error: "the size for values of type `dyn Trait` cannot be known at compilation time".