rust 是什么让某个东西成为“特征对象”?

6yoyoihd  于 2023-01-02  发布在  其他
关注(0)|答案(4)|浏览(172)

最近Rust的变化使“trait对象”对我来说更加突出,但是我对trait对象的实际构成只有模糊的理解,其中一个变化是upcoming change,它允许trait对象将trait实现转发到内部类型。
给定trait Foo,我非常确定Box<Foo>/Box<dyn Foo>是trait对象。&Foo/&dyn Foo也是trait对象吗?其他智能指针如RcArc又如何呢?我怎样才能创建自己的类型来算作trait对象呢?
reference只提到trait对象一次,但不像定义。

dohp0rv5

dohp0rv51#

当你有一个指向trait的指针时,你就有了trait对象。BoxArcRc和引用&在它们的核心都是指针。在定义一个“trait对象”方面,它们的工作方式是相同的。
“Trait对象”是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

ecfsfe2w

ecfsfe2w2#

简短回答:您只能将对象安全的trait创建为trait对象。
物品安全特性:不能解析为具体实现类型的特性。实际上,有两个规则控制特性是否是对象安全的。

1.返回类型不是Self。
1.没有泛型类型参数。
任何满足这两个规则的trait都可以用作trait对象。
对象安全的trait示例可用作trait对象

trait Draw {
    fn draw(&self);
}

不能用作特征对象的特征示例:

trait Draw {
    fn draw(&self) -> Self;
}

详细说明:https://doc.rust-lang.org/book/second-edition/ch17-02-trait-objects.html

gfttwv5a

gfttwv5a3#

trait对象是动态分派的Rust实现。动态分派允许在运行时选择多态操作(trait方法)的一个特定实现。动态分派允许非常灵活的架构,因为我们可以在运行时交换函数实现。然而,与动态分派相关的运行时开销很小。
保存trait对象的变量/参数是胖指针,由以下组件组成:

  • 指向内存中对象的指针
  • 指向该对象的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);
}
qxsslcnc

qxsslcnc4#

关于trait对象是什么,这个问题已经有了很好的答案,让我在这里给予一个例子,关于什么时候我们可能想要使用trait对象,为什么要使用trait对象,我将基于Rust Book中给出的例子。
假设我们需要一个GUI库来创建一个GUI表单。该GUI表单将由可视组件组成,如按钮、标签、复选框等。让我们问问自己,谁应该知道如何绘制给定的组件?库还是组件本身?如果库附带了您可能需要的所有组件的固定集合,那么它可以在内部使用一个枚举,其中每个枚举变量表示一个组件类型,并且库本身可以处理所有绘图(因为它知道关于其组件的所有信息以及应该如何精确地绘制它们)。然而,如果库允许您使用第三方组件或您自己编写的组件,那就更好了。
在Java、C#、C++等面向对象的语言中,这通常是通过组件层次结构来实现的,其中所有组件都继承一个基类(我们称之为Component)。Component类将有一个draw()方法(甚至可以定义为abstract,以便强制所有子类实现该方法)。
然而,Rust没有继承。Rust枚举非常强大,因为每个枚举变体可以有不同类型和数量的关联数据,它们经常用于在典型的OOP语言中使用继承的情况。在Rust中使用枚举和泛型的一个重要优点是,所有的东西在编译时都是已知的。这意味着您无需牺牲性能(不需要像vtables这样的东西)。但是在某些情况下,就像我们的例子中一样,枚举没有提供足够的灵活性。库需要跟踪components of different type,并且需要一种方法来调用它甚至不知道的组件上的方法。正如其他人所解释的,trait对象通常被称为dynamic dispatch,它是Rust执行动态分派的方式。

相关问题