Объекты трейтов (trait objects) в языке Rust позволяют выбирать конкретную реализацию трейта во время выполнения. При использовании объектов трейтов по сути применяется динамическая диспетчеризация — процесс, который во время выполнения определяет, какую реализацию метода следует вызвать на основе фактического типа объекта. В этом отличие от статической диспетчеризации, где вызовы методов разрешаются во время компиляции.
Для определения объекта трейта применяется слово dyn
let переменная: &dyn трейт
Рассмотрим пример:
trait Shape {
fn area(&self) -> f64; // метод вычисления площади фигуры
}
// структура круг
struct Circle {
radius: f64 // радиус
}
// структура прямоугольник
struct Rectangle {
width: f64, // ширина
height: f64 // высота
}
// реализации вычисления площади для круга
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * 3.14
}
}
// реализации вычисления площади для прямоугольника
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let mut my_shape: &dyn Shape = &Circle { radius: 5.0 }; // определяем трейт объект, который хранит ссылку на структуру Circle
println!("Circle area: {}", my_shape.area()); // вызываем метод area у объекта трейта my_shape
my_shape = &Rectangle {width:10.0, height: 20.0}; // изменяем значение объекта трейта
println!("Rectangle area: {}", my_shape.area());
}
Здесь у нас есть трейт Shape, который определяет геометрическую фигуру и метод для вычисления ее площади.
Также есть две структуры - Circle и Rectangle, которые представляют круг и прямоугольник соответственно и которые по своему реализуют трейт Shape.
В функции main мы создаем объект трейта my_shape, который представляет изменяемую переменную - объект трейта Shape. Для его определения применяется ключевое слово dyn
let mut my_shape: &dyn Shape = &Circle { radius: 5.0 };
Эта переменная инициализируется ссылкой на объект Circle. Стоит отметить, что для целей демонстрации здесь переменная определена как изменяемая, но в принципе это не обязательно. Главное, что она представляет трейт Shape. И, как и в общем случае, далее мы можем вызвать у нее метод area:
println!("Circle area: {}", my_shape.area()); // вызываем метод area у объекта трейта my_shape
И поскольку переменная является изменяемой, то мы можем присвоить ей ссылку на другой объект, который реализует трейт Shape, например, ссылку на Rectangle:
my_shape = &Rectangle {width:10.0, height: 20.0};
И далее также можем вызывать метод area.
Стоит отметить, что мы не можем просто присвоить переменной ссылку на Circle, а потом на Rectangle:
let mut my_shape = &Circle { radius: 5.0 };
........................
my_shape = &Rectangle {width:10.0, height: 20.0}; // ! Ошибка
В этом случае мы получим ошибку, так как тип переменной сразу определяется как Circle.
Аналогичным образом мы можем использовать объекты трейтов в функциях в качестве параметров:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64
}
struct Rectangle {
width: f64,
height: f64
}
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * 3.14
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn print_area(shape: &dyn Shape) { // shape - объект трейта Shape
println!("Area: {}", shape.area());
}
fn main() {
let circle = Circle { radius: 5.0 };
print_area(&circle); // Area: 78.5
let rect = Rectangle {width:10.0, height: 20.0};
print_area(&rect); // Area: 200
}
Здесь определена функция print_area, которая принимает объект трейта Shape и выводит его площадь.
Объекты трейтов позволяют добиться полиморфизма во время выполнения, делая код более гибким и адаптируемым к различным типам, сохраняя при этом общий признак. В то же время эта гибкость и динамическая диспетчеризация приводит к незначительному снижению производительности по сравнению со статической диспетчеризацией.