dyn, impl and Trait Objects

734 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


让我们看看下面的代码:

trait Animal {
   fn talk(&self);
}
struct Cat {}
struct Dog {}

impl Animal for Cat {
    fn talk(&self) {
            println!(“meow”);
    }
}
impl Animal for Dog {
    fn talk(&self) {
            println!(“bark”);
    }
}
// dyn method
fn animal_talk(a: &dyn Animal) {
    a.talk();
}

fn main() {
    let d = Dog {};
    let c = Cat {};
    animal_talk(&d);
    animal_talk(&c);
}

可以看到 animal_talk()cat and dog 都被调用了。**dyn** 告诉编译器不用确定确切的类型,只需要满足于对某个实现动物trait的类型的引用。这被称为动态派发,类型在运行时被确定,所以有一个运行时的开销。

但为什么在dyn之前有一个&? "指向一个特性的指针?" 没错! 因为类型(以及具体类型的大小)在编译时是不知道的;仅仅使用以下是非法的:

fn animal_talk(a: dyn Animal) {
   a.talk();
}

编译器会报错:大小不详。 Rust 称其为 trait对象(&dyn Animal)。它表示一个指向具体类型的指针和一个指向函数指针的 vtable pointerBox<dyn Animal>, Rc<dyn Animal> 也是 trait对象。它们也包含一个指向在堆上分配的具体类型的指针,该类型满足给定的 trait。 现在,我们看一下下面的代码:

fn animal_talk(a: impl Animal) {
    a.talk();
}

fn main() {
    let c = Cat{};
    let d = Dog{};

    animal_talk(c);
    animal_talk(d);
}

上面代码的编译结果是OK,而且这里没有&impl 使编译器在编译时确定类型,这意味着编译器将做一些函数名称处理,并将有两个函数的变体:一个是 dog,另一个是 mat。这被称为:单态化,不会有任何运行时的开销,但会导致代码膨胀 (相同的代码被多次实现)。 考虑一下下面的代码,这是否会被编译?

fn animal () -> impl Animal {
    if (is_dog_available()) {
        return Dog {};
    }
    Cat {}
}

编译失败了! 因为,这里的类型是在编译时确定的(静态派发)。这个错误不言而喻:

error[E0308]: mismatched types
   |
   | fn animal() -> impl Animal {
   |                                   ----------- expected because this return type...
   |     if (is_dog_available()) {
   |         return Dog {};
   |                ------ ...is found to be `Dog` here
   |     } 
   |             
...         
   |     Cat {}
   |     ^^^^^^ expected struct `Dog`, found struct `Cat`
   |
   = note: expected type `Dog`
              found type `Cat`

编译器在这里首先将 Dog 作为返回类型归零,并期望在函数体中的其他返回类型也是同样类型。 那下面的代码呢?

fn animal() -> Box<dyn Animal> {
    if (is_dog_available()) {
        return Box::new(Dog {});
    } 

    Box::new(Cat {})
}

现在编译结果是OK的,因为它不需要知道确切的返回类型(动态调度)。它所需要的只是一些满足给定特征的 Box 类型(Trait Object)。

对象安全

如果一个trait在它的方法中,实现它的类型作为一个值(而不是作为一个引用)被认为是不安全的对象。 考虑一下下面的代码:

trait Animal {
    fn nop(self) {}
}

struct Cat {}
impl Animal for Cat {}
	
fn main() {
    let c = Cat {};
    let ct:&dyn Animal = &c;
    c.nop();
}

会有以下错误:

error[E0277]: the size for values of type `Self` cannot be known at compilation time
  |
  |     fn nop(self)  {}
  |            ^^^^ - help: consider further restricting `Self`: `where Self: std::marker::Sized`
  |            |
  |            doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `Self`

nop() 接收传值。但由于这是动态派发,类型和它的大小是不知道的。即使我们给 Self 加上 Sized 约束,即 fn nop(self) where Self:Sized {},编译器仍然会报错,因为在编译时没有办法知道大小,而且还会在代码中引入对 Self 的依赖(即把它当做一个值传入)。