Что такое trait object (dyn Trait) и в чём разница между static dispatch и dynamic dispatch?
Static dispatch (impl Trait / generics) компилятор разворачивает в отдельные копии функции для каждого конкретного типа — нет накладных расходов в рантайме. Dynamic dispatch (dyn Trait) хранит fat pointer (данные + vtable) и разрешает вызов метода в рантайме через указатель на таблицу виртуальных функций.
Static dispatch — monomorphization
Когда вы пишете обобщённую функцию с trait bound, компилятор создаёт отдельную копию для каждого конкретного типа. Это называется monomorphization:
fn print_area<T: Shape>(shape: &T) {
println!("area = {}", shape.area());
}
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Shape for Rect {
fn area(&self) -> f64 { self.w * self.h }
}
fn main() {
// Компилятор генерирует print_area_Circle и print_area_Rect
print_area(&Circle { radius: 3.0 });
print_area(&Rect { w: 4.0, h: 5.0 });
}
Вызов метода shape.area() разрешается на этапе компиляции — это обычный прямой вызов функции. Нулевые накладные расходы в рантайме, компилятор может инлайнить.
Dynamic dispatch — dyn Trait и vtable
Trait object &dyn Shape или Box<dyn Shape> — это fat pointer: два указателя по 8 байт каждый.
- data pointer — указатель на сами данные объекта.
- vtable pointer — указатель на таблицу виртуальных функций конкретного типа.
Vtable содержит: размер типа, выравнивание, указатель на drop, и указатели на каждый метод трейта в том порядке, в котором они объявлены.
fn print_area_dyn(shape: &dyn Shape) {
// Вызов через vtable: *(shape.vtable.area)(shape.data)
println!("area = {}", shape.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Rect { w: 4.0, h: 5.0 }),
];
for s in &shapes {
print_area_dyn(s.as_ref());
}
}
Сравнение: когда что использовать
- Static dispatch: предпочтителен по умолчанию. Ноль накладных расходов, возможность инлайнинга, лучшая оптимизация. Недостаток: code bloat при большом числе типов и сложных дженериках.
- Dynamic dispatch: нужен, когда набор типов не известен на этапе компиляции (плагины, heterogeneous collections, callback). Накладные расходы: один indirect call + потенциальный cache miss по vtable (~1–5 нс).
Object safety
Не каждый трейт можно использовать как dyn Trait. Трейт object-safe, только если:
- Ни один метод не возвращает
Self. - Ни один метод не имеет дженерик-параметра.
- Трейт не требует
Self: Sized.
trait NotObjectSafe {
fn clone_self(&self) -> Self; // возвращает Self — нельзя dyn
}
trait ObjectSafe {
fn describe(&self) -> String; // OK
fn area(&self) -> f64; // OK
}
impl Trait в позиции возвращаемого типа
// Статический диспатч: конкретный тип скрыт, но один
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// Динамический диспатч: может быть любой тип в рантайме
fn make_shape(circle: bool) -> Box<dyn Shape> {
if circle {
Box::new(Circle { radius: 1.0 })
} else {
Box::new(Rect { w: 2.0, h: 3.0 })
}
}
Подводные камни
- Code bloat: monomorphization создаёт копию функции для каждого типа. В крупных проектах это замедляет компиляцию и раздувает бинарь. Решение: использовать dyn Trait во внутренних вспомогательных функциях и публичную generic-обёртку сверху.
- dyn Trait не реализует сам трейт: нельзя передать
&dyn Shapeтуда, где ожидаетсяT: Shapeбез явного враппера. - Размер dyn Trait не известен: нельзя хранить
dyn Shapeна стеке напрямую — только за указателем (&dyn,Box<dyn>,Arc<dyn>). - Send + Sync с dyn: чтобы
Box<dyn Shape>было Send, нужно явно указатьBox<dyn Shape + Send>— иначе компилятор откажет при передаче в поток. - Lifetime в dyn: по умолчанию
dyn Traitимеет неявный lifetime'static. При работе с заимствованными данными нужноdyn Trait + 'a. - Downcast: из
dyn Traitнельзя получить конкретный тип безAny+downcast_ref. Архитектурно это признак плохого дизайна. - Производительность vtable: indirect call ломает branch prediction и inline cache процессора. В tight loops лучше static dispatch или enum dispatch.
- enum dispatch как альтернатива: если набор типов конечен и известен, enum с вариантами часто быстрее dyn Trait и не требует аллокации на куче.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.