Что делает что-то "объектом черты"?
Недавние изменения в Rust сделали "объекты-черты" более заметными для меня, но у меня есть только туманное понимание того, что на самом деле превращает что-то в объект-черту. Одним из изменений, в частности, является предстоящее изменение, позволяющее объектам признаков перенаправлять реализации признаков во внутренний тип.
Учитывая черту Foo
Я уверен, что Box<Foo>
это черта объекта. Является &Foo
также признак объекта? А как насчет других умных указателей, таких как Rc
или же Arc
? Как я могу создать свой собственный тип, который будет считаться объектом черты?
Ссылка упоминает только признаки свойств один раз, но ничего похожего на определение.
4 ответа
У вас есть объекты черты, когда у вас есть указатель на черту. Box
, Arc
, Rc
и ссылка &
все, по своей сути, указатели. С точки зрения определения "объекта черты" они работают одинаково.
"Объекты черты" - это стремление Руста к динамической отправке. Вот пример, который, я надеюсь, поможет показать, что представляют собой черты объектов:
// define an example struct, make it printable
#[derive(Debug)]
struct Foo;
// an example trait
trait Bar {
fn baz(&self);
}
// implement the trait for Foo
impl Bar for Foo {
fn baz(&self) { println!("{:?}", self) }
}
// This is a generic function that takes any T that implements trait Bar.
// It must resolve to a specific concrete T at compile time.
// The compiler creates a different version of this function
// for each concrete type used to call it so &T here is NOT
// a trait object (as T will represent a known, sized type
// after compilation)
fn static_dispatch<T>(t: &T) where T:Bar {
t.baz(); // we can do this because t implements Bar
}
// This function takes a pointer to a something that implements trait Bar
// (it'll know what it is only at runtime). &dyn Bar is a trait object.
// There's only one version of this function at runtime, so this
// reduces the size of the compiled program if the function
// is called with several different types vs using static_dispatch.
// However performance is slightly lower, as the &dyn Bar that
// dynamic_dispatch receives is a pointer to the object +
// a vtable with all the Bar methods that the object implements.
// Calling baz() on t means having to look it up in this vtable.
fn dynamic_dispatch(t: &dyn Bar) {
// ----------------^
// this is the trait object! It would also work with Box<dyn Bar> or
// Rc<dyn Bar> or Arc<dyn Bar>
//
t.baz(); // we can do this because t implements Bar
}
fn main() {
let foo = Foo;
static_dispatch(&foo);
dynamic_dispatch(&foo);
}
Для дальнейшего использования, есть хорошая глава "Объекты черты" в книге Rust
Краткий ответ: Вы можете сделать объектобезопасные черты только объектами черт.
Объектно-безопасные черты: черты, которые не соответствуют конкретному типу реализации. На практике два правила определяют, является ли черта объектобезопасной.
- Тип возвращаемого значения не Self.
- Нет общих параметров типа.
Любая черта, удовлетворяющая этим двум правилам, может быть использована в качестве объекта черты.
Пример признака, который является объектно-безопасным, может использоваться в качестве объекта признака:
trait Draw {
fn draw(&self);
}
Пример признака, который нельзя использовать в качестве объекта признака:
trait Draw {
fn draw(&self) -> Self;
}
Для подробного объяснения: https://doc.rust-lang.org/book/second-edition/ch17-02-trait-objects.html
Трейт-объекты — это реализация динамической диспетчеризации в Rust. Динамическая диспетчеризация позволяет выбрать одну конкретную реализацию полиморфной операции (методы признаков) во время выполнения. Динамическая диспетчеризация обеспечивает очень гибкую архитектуру, потому что мы можем менять реализации функций во время выполнения. Однако динамическая диспетчеризация связана с небольшими затратами времени выполнения.
Переменные/параметры, содержащие трейт-объекты, представляют собой толстые указатели, состоящие из следующих компонентов:
- указатель на объект в памяти
- указатель на виртуальную таблицу этого объекта, виртуальная таблица - это таблица с указателями, которые указывают на фактическую реализацию (ы) метода (ов).
Пример
struct Point {
x: i64,
y: i64,
z: i64,
}
trait Print {
fn print(&self);
}
// dyn Print is actually a type and we can implement methods on it
impl dyn Print + 'static {
fn print_traitobject(&self) {
println!("from trait object");
}
}
impl Print for Point {
fn print(&self) {
println!("x: {}, y: {}, z: {}", self.x, self.y, self.z);
}
}
// static dispatch (compile time): compiler must know specific versions
// at compile time generates a version for each type
// compiler will use monomorphization to create different versions of the function
// for each type. However, because they can be inlined, it generally has a faster runtime
// compared to dynamic dispatch
fn static_dispatch<T: Print>(point: &T) {
point.print();
}
// dynamic dispatch (run time): compiler doesn't need to know specific versions
// at compile time because it will use a pointer to the data and the vtable.
// The vtable contains pointers to all the different different function implementations.
// Because it has to do lookups at runtime it is generally slower compared to static dispatch
// point_trait_obj is a trait object
fn dynamic_dispatch(point_trait_obj: &(dyn Print + 'static)) {
point_trait_obj.print();
point_trait_obj.print_traitobject();
}
fn main() {
let point = Point { x: 1, y: 2, z: 3 };
// On the next line the compiler knows that the generic type T is Point
static_dispatch(&point);
// This function takes any obj which implements Print trait
// We could, at runtime, change the specfic type as long as it implements the Print trait
dynamic_dispatch(&point);
}
На этот вопрос уже есть хорошие ответы о том, что такое трейт-объект. Позвольте мне привести здесь пример того, когда мы можем захотеть использовать трейт-объекты и почему . Я буду основывать свой пример на примере, приведенном в Rust Book.
Допустим, нам нужна библиотека графического интерфейса для создания формы графического интерфейса. Эта форма GUI будет состоять из визуальных компонентов, таких как кнопки, метки, флажки и т. д. Давайте спросим себя, кто должен уметь рисовать данный компонент? Библиотека или сам компонент? Если бы библиотека поставлялась с фиксированным набором всех компонентов, которые вам могут когда-либо понадобиться, то она могла бы внутренне использовать перечисление, где каждый вариант перечисления представляет один тип компонента, а сама библиотека могла бы позаботиться обо всем рисовании (поскольку она знает все о его компоненты и как именно они должны быть нарисованы). Однако было бы намного лучше, если бы библиотека позволяла вам также использовать сторонние компоненты или те, которые вы написали сами.
В языках ООП, таких как Java, C#, C++ и других, это обычно делается с помощью иерархии компонентов, в которой все компоненты наследуют базовый класс (назовем его ). ЧтоComponent
класс будет иметьdraw()
метод (который можно было бы даже определить какabstract
, чтобы заставить все подклассы реализовать этот метод).
Однако в Rust нет наследования. Перечисления Rust очень эффективны, так как каждый вариант перечисления может иметь разные типы и количество связанных данных, и они часто используются в ситуациях, когда вы используете наследование в типичном языке ООП. Важным преимуществом использования перечислений и дженериков в Rust является то, что все известно во время компиляции, а это означает, что вам не нужно жертвовать производительностью (нет необходимости в таких вещах, как vtables). Но в некоторых случаях, как в нашем примере, перечисления не обеспечивают достаточной гибкости. Библиотека должна отслеживать компоненты разных типов , и ей нужен способ вызова методов для компонентов, о которых она даже не знает. Обычно это называется динамической отправкой.и, как объясняли другие, трейт-объекты — это способ динамической диспетчеризации в Rust.