是啥使某物成为“特征对象”?

Posted

技术标签:

【中文标题】是啥使某物成为“特征对象”?【英文标题】:What makes something a "trait object"?是什么使某物成为“特征对象”? 【发布时间】:2015-02-18 11:53:58 【问题描述】:

最近的 Rust 更改使“特征对象”对我来说更加突出,但我对究竟是什么使某些东西成为特征对象只有模糊的把握。一项特别的更改是 upcoming change 允许 trait 对象将 trait 实现转发到内部类型。

给定一个特征Foo,我很确定Box<Foo> / Box<dyn Foo> 是一个特征对象。 &Foo / &dyn Foo 也是 trait 对象吗?像RcArc 这样的其他智能指针呢?我怎样才能使自己的类型算作 trait 对象?

reference 只提到一次 trait 对象,但没有像定义一样。

【问题讨论】:

【参考方案1】:

Trait 对象是动态调度的 Rust 实现。动态分派允许在运行时选择多态操作(特征方法)的一种特定实现。动态调度允许非常灵活的架构,因为我们可以在运行时交换函数实现。但是,动态调度的运行时成本很小。

保存特征对象的变量/参数是胖指针,由以下组件组成:

指向内存中对象的指针 指向该对象的 vtable 的指针,vtable 是一个带有指向实际方法实现的指针的表。

示例

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);

【讨论】:

【参考方案2】:

当你有一个指向特性的指针时,你就有了特性对象。 BoxArcRc 和引用 &amp; 本质上都是指针。在定义“特征对象”方面,它们的工作方式相同。

“特征对象”是 Rust 对 dynamic dispatch 的看法。 这是一个示例,我希望能帮助说明什么是 trait 对象:

// 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);

供进一步参考,有一个很好的Trait Objects chapter of the Rust book

【讨论】:

谢谢,这似乎是一个全面的答案。创建我自己的可以像 trait 对象一样工作的类型怎么样? @Shepmaster,类型不像特征对象那样“行为”;而是任何指向特征的指针都是特征对象,并且可以有不同种类的指针。 Box&lt;T&gt; 是一个拥有指针,Rc&lt;T&gt; 是一个共享所有权指针,Arc&lt;T&gt; 是一个多线程共享所有权指针等。原则上,这些都可以用来定义 trait 对象,但目前只有引用和Boxes 为此工作。所以不,现在你不能创建可用于创建特征对象的自定义指针类型。 @Shepmaster,不,这不完全正确。 Box&lt;Trait&gt;/possible Rc&lt;Trait&gt; trait 对象本身,它们不会被转换或提供&amp;Trait @Lii 我认为没有实际区别。术语“特征对象”可以适用于两者,通常不会引起混淆。我想说,从语义上讲,它确实更多地引用了指针背后的值。但是如果需要严格区分胖指针和它所指向的值,我通常称它们为“特征对象指针”和“特征对象指针指向的值”。 > 术语“特征对象”可以适用于两者,通常不会引起混淆。 FWIW,我对此感到困惑:)模棱两可的用法感觉就像特征对象=指向数据的胖指针+ vtable,但是这些胖指针同时也应该以某种方式未调整大小,这不会使感觉。幸运的是,the Rust reference is currently clear about this:未调整大小的值dyn Trait 本身是一个特征对象,必须在某种指针后面使用(&amp;dyn TraitBox&lt;dyn Trait&gt; 等)。【参考方案3】:

简短回答:您只能将对象安全的特征转换为特征对象。

对象安全特征:无法解析为具体实现类型的特征。在实践中,两个规则控制一个特征是否是对象安全的。

    返回类型不是 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

【讨论】:

更一般地说,不在对象级别的所有内容(也称为使用Self)都会使特征不安全。例如,如果您的 trait 有一个 const 成员或一个没有 self 作为第一个参数的函数。

以上是关于是啥使某物成为“特征对象”?的主要内容,如果未能解决你的问题,请参考以下文章

是啥使请求成为asp.net C#中的新请求

这个函数是啥使它不是函数

Angular 2:啥使服务成为“外部”角度区域?

谁能给我一个关于“啥使对象有状态”的这种情况的好例子?

java中的“x =(某物)”是啥意思?

SwiftUI 中的“身份”是啥意思以及我们如何更改某物的“身份”