Трейт-объекты в Rust: dyn и &dyn
Использование dyn Trait и &dyn Trait — это одна из сложных тем для понимания в Rust. Давайте разберём на простых примерах.
dyn Trait — трейт-объект (работает через указатель)
Ключевой момент: dyn Trait — это трейт-объект - тип с динамической диспетчеризацией (dynamic dispatch). Сам по себе он не имеет размера (unsized), поэтому его нельзя хранить напрямую в стеке. Трейт-объект - это всегда ссылка на какой-либо объект, реализующий данный трейт и указатель на таблицу vtable - методов реализации данного трейта для данного конкретного объекта.
Рассмотрим реализацию трейта:
trait Animal {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) -> String { "Woof!".to_string() }
}
impl Animal for Cat {
fn speak(&self) -> String { "Meow!".to_string() }
}
Когда использовать &dyn Trait
Передача ссылки на трейт-объект
// принимает ссылку на любой тип, реализующий Animal
fn make_speak(animal: &dyn Animal) -> String {
animal.speak()
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", make_speak(&dog)); // Woof!
println!("{}", make_speak(&cat)); // Meow!
// Можно даже в массив (но нужно указать тип явно):
let animals: [&dyn Animal; 2] = [&dog, &cat];
}
Почему &dyn:
- Мы передаём ссылку на объект + vtable для динамического вызова
- Сам объект (Dog/Cat) живёт где-то ещё (в стеке или куче)
&dyn Animalимеет фиксированный размер - два указателя: указатель на объект + указатель на vtable
Когда использовать Box<dyn Trait>
Возврат трейт-объекта из функции
// ВОЗВРАЩАЕТ трейт-объект в куче
fn create_animal(name: &str) -> Box<dyn Animal> {
match name {
"dog" => Box::new(Dog),
"cat" => Box::new(Cat),
_ => panic!("Unknown animal")
}
}
fn main() {
let animal: Box<dyn Animal> = create_animal("dog");
println!("{}", animal.speak()); // Woof!
}
Почему Box<dyn>:
- Трейт-объект нужно где-то хранить (в куче)
- Box — умный указатель, который владеет данными в куче
- Позволяет возвращать разные типы из функции
Не используется dyn Trait напрямую
Напрямую трей-объекты не используются, потому что должны быть связаны со ссылкой на какой-то объект, а сами не имеют размера, но компилятор должен знать размер объекта при компиляции. Поэтому всегда используются в связке с каким-либо указателем.
// Например, если нужно хранить разные типы в одной коллекции:
fn main() {
let dog = Dog;
let cat = Cat;
// Вектор трейт-объектов (ссылок)
let animals_vec: Vec<&dyn Animal> = vec![&dog, &cat];
// Или вектор boxed трейт-объектов
let animals_boxed: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat)
];
}
Если необходимо передать объект, реализующий трейт, с передачей владения, то вместо dyn можно использовать impl:
// принимает любой объект типа, реализующего Animal
fn make_speak_impl(animal: impl Animal) -> String {
animal.speak()
}
или использовать обобщённый типом, реализующий трейт:
// или generic-тип, реализующий Animal
fn make_speak_generic<T: Animal>(animal: T) -> String {
animal.speak()
}
Сравнение &dyn vs Box<dyn>
| &dyn Trait | Box<dyn Trait> | |
|---|---|---|
| Владение | Заимствует (не владеет) |
Владеет (перемещает в кучу) |
| Память | Два указателя в стеке |
Указатель на кучу + vtable |
| Производительность | Быстрее (нет аллокации) |
Медленнее (аллокация в куче) |
| Использование | Параметры функций, временные ссылки |
Возврат из функций, поле структуры |
| Lifetime | Нужен явный |
'static по умолчанию |
Практические примеры
Структура с трейт-объектом
struct Zoo {
// Храним boxed трейт-объекты
animals: Vec<Box<dyn Animal>>,
}
impl Zoo {
fn add_animal(&mut self, animal: Box<dyn Animal>) {
self.animals.push(animal);
}
fn make_all_speak(&self) {
for animal in &self.animals {
println!("{}", animal.speak());
}
}
}
Функция принимающая разные типы
fn process_animals(animals: &[&dyn Animal]) {
// варианты:
// for &animal in animals {
// for animal in animals.iter() {
for animal in animals {
println!("Animal says: {}", animal.speak());
}
}
fn main() {
let dog = Dog;
let cat = Cat;
let animals: [&dyn Animal; 2] = [&dog, &cat];
process_animals(&animals);
}
Важное правило: Никогда не используется просто dyn Trait в объявлении переменной или параметра функции. Всегда будет одна из обёрток:
// ПРАВИЛЬНО: let a: &dyn Animal = &dog; let b: Box<dyn Animal> = Box::new(cat); let c: Rc<dyn Animal> = Rc::new(dog); let d: Arc<dyn Animal> = Arc::new(cat); // НЕПРАВИЛЬНО (не скомпилируется): let e: dyn Animal = dog; // Ошибка: `dyn Animal` не имеет размера
Когда что выбирать
Выбирайте &dyn Trait когда:
- Нужно только прочитать данные
- Объект уже живёт где-то (стек, куча)
- Не хотите аллокаций в куче
- Работате с временными объектами
Выбирайте Box<dyn Trait> когда:
- Нужно владеть объектом
- Возвращать из функции
- Хранить в структуре
- Нужно перемещать между потоками (с Send + Sync)
Итог
&dyn Trait— ссылка на трейт-объект (заимствование)Box<dyn Trait>— владение трейт-объектом в куче- просто
dyn Trait— никогда не используется напрямую, всегда внутри указателя/ссылке