Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Трейт-объекты в 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 — никогда не используется напрямую, всегда внутри указателя/ссылке