Что означает ключевое слово `dyn` в типах Rust?
Что такое dyn и зачем он нужен?
Ключевое слово dyn в Rust — это синтаксический маркер, который указывает на динамическую диспетчеризацию (dynamic dispatch). Оно появилось в Rust 2018 edition для явного обозначения trait-объектов. Раньше можно было писать просто Box, но это было неоднозначно, и компилятор не всегда мог понять, имеете ли вы в виду конкретный тип или трейт. Теперь dyn обязательно для trait-объектов.
Статическая vs динамическая диспетчеризация
Чтобы понять dyn, нужно разобраться в двух подходах:
- Статическая диспетчеризация (мономорфизация) — когда компилятор создаёт отдельную копию функции для каждого конкретного типа. Это происходит с дженериками:
fn foo<T: Animal>(t: T). Компилятор генерирует код для каждого T, который вы используете. Быстро, но увеличивает размер бинарника. - Динамическая диспетчеризация — когда тип известен только во время выполнения. Вы работаете с указателем на трейт (trait object), и вызов метода происходит через виртуальную таблицу (vtable). Это медленнее (один косвенный вызов), но позволяет хранить разные типы в одной коллекции.
Как работает dyn на практике
Когда вы пишете Box<dyn Animal>, вы создаёте trait object. Это жирный указатель (fat pointer), который состоит из двух частей:
- Указатель на сами данные (например, на структуру
Dog). - Указатель на vtable — таблицу с адресами всех методов трейта для этого конкретного типа.
Пример кода с пояснениями:
trait Animal {
fn speak(&self);
fn name(&self) -> &str;
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("{} говорит: Гав!", self.name);
}
fn name(&self) -> &str {
&self.name
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn speak(&self) {
println!("{} говорит: Мяу!", self.name);
}
fn name(&self) -> &str {
&self.name
}
}
fn main() {
// Вектор с trait objects — можно хранить разных животных
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Шарик".to_string() }),
Box::new(Cat { name: "Мурка".to_string() }),
];
for animal in &animals {
animal.speak(); // Вызов через vtable — динамическая диспетчеризация
}
}
Когда использовать dyn?
- Нужно: когда вы хотите хранить разные типы, реализующие один трейт, в одной коллекции (вектор, хеш-таблица и т.д.).
- Нужно: когда тип определяется во время выполнения (например, конфигурация из файла).
- Не нужно: когда вы знаете точный тип на этапе компиляции — используйте дженерики
impl Traitилиfn foo<T: Trait>(t: T). Это быстрее.
Важные ограничения trait-объектов
Не все трейты можно превратить в trait-объекты (то есть использовать с dyn). Трейт должен быть object-safe. Основные правила:
- Методы не могут возвращать
Self(конкретный тип). - Методы не могут иметь обобщённые параметры (дженерики).
- Трейт не должен наследовать от
Sized(но это редкость).
Пример не object-safe трейта:
trait Clone {
fn clone(&self) -> Self; // Self недопустим для dyn Clone
}
// Ошибка: Box<dyn Clone> не скомпилируется
Итог
Ключевое слово dyn — это явное указание компилятору: "я хочу динамическую диспетчеризацию, используй vtable". Оно обязательно для trait-объектов. Без него компилятор не поймёт, что вы имеете в виду трейт, а не конкретный тип. Используйте dyn только когда вам действительно нужна гибкость в рантайме, в остальных случаях отдавайте предпочтение статической диспетчеризации (дженерикам).
Альтернативный взгляд: dyn как явный контракт
Можно думать о dyn как о способе сделать код самодокументируемым. Раньше (до Rust 2018) можно было писать Box<Animal>, и было неясно: это конкретный тип Animal или трейт? Теперь синтаксис чётко разделяет:
Box<dyn Animal>— ящик с любым типом, реализующим Animal.Box<Animal>— ящик с конкретной структурой Animal (если такая существует).
Ещё один полезный приём: если вы пишете библиотеку и хотите скрыть конкретный тип за абстракцией, используйте dyn. Это позволяет менять реализацию без изменения API:
pub trait Logger {
fn log(&self, msg: &str);
}
pub struct App {
logger: Box<dyn Logger>, // Пользователь может подставить любой логгер
}
impl App {
pub fn new(logger: Box<dyn Logger>) -> Self {
App { logger }
}
}
Однако помните о производительности: каждый вызов метода через dyn — это косвенный вызов через vtable. Для hot-путей (критичных по скорости участков кода) лучше использовать дженерики. Для конфигурационных или редко вызываемых мест — dyn вполне подходит.