在上一节中有一段代码无法经过编译:

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        Tweet { ... }  // 此处不能回来两个不同的类型
    } else {
        Post { ... }  // 此处不能回来两个不同的类型
    }
}

其中PostTweet都完成了Summarytrait,因此上面的函数试图经过回来impl Summary来回来这两个类型,可是编译器却报错了,原因是impl Trait的回来值类型并不支撑多种不同的类型回来,那假如咱们想回来多种类型,该怎么办?这时候就需求用到 trait 目标。

trait 目标的界说

trait 是一种束缚,而不是详细类型,它归于DST(类似于str),其 size 无法在编译阶段确认,只能经过指针来直接拜访,能够经过&Trait引证或者Box<Trait>智能指针的方式来运用。

Rust 顶用一个胖指针(除了一个指针外,还包括别的一些信息),来表示 一个指向 trait 的引证 (类似于str&str),即 trait object,分别指向 data 与 vtable。简单讲,trait object包括data指针和vtable指针。

注:什么是 Trait Object 呢?指向 trait 的指针便是Trait Object。假如 Summary 是一个 trait 的名称,那么 &SummaryBox<Summary> 都是一种 Trait Object,是“胖指针“。

Rust 中的 trait 对象

trait object reference

pub struct TraitObjectReference { // trait object 胖指针
    pub data: *mut (), // data指针
    pub vtable: *mut (), // vtable指针
}
// vtable 本质上是一个结构体指针
struct Vtable {
    destructor: fn(*mut ()),
    size: usize,
    alignment: usize,
    fn_1: fn(*const ()),
    fn_2: fn(*const ()),
    fn3_n: fn(*const ()),
}

注:每个vtable中的destructor字段都指向一个函数,该函数将清理vtable类型的任何资源。sizealign字段存储了被清除类型的巨细,以及它的对齐要求。destructorsizealignment这三个字段是每个vtable都共有的类型。fn_1fn_n便是 在trait中界说的办法。比方trait_object.fn_1()这样的办法调用将从vtable中检索出正确的指针,然后对其进行动态调用。

vtablevirtual method table的缩写。 本质上是结构体指针的结构,指向详细完成中每种办法的一段机器代码。在 Rust 中,当运用多态性时,编译器会主动为每个带有虚函数的类型创立一个虚表。在运行时,这个虚表会被动态分配内存,并用于存储虚函数的地址虚拟表只在编译时生成一次,由同类型的一切目标同享。

Rust 中的 trait 对象

多 trait 时 vtable 示意图

当调用一个 trait object 的办法时,rust 会主动运用虚拟办法表,以确认调用哪个办法的完成。

dyn 关键

dyn关键字只用在 trait 目标的类型声明上,常见形式有Box<dyn trait>&dyn trait等。

以 Shape trait 为例:

use std::f64::consts::PI;
// 圆形
struct Circle {
    radius: f64,
}
// 正方形
struct Square { 
    side: f64
}
/// 声明一个图形 shape trait
trait Shape { 
    fn area(&self) -> f64;
}
impl Circle {  
   fn new(radius: f64) -> Self {  
    Circle { radius }
   }  
}
/// 为 Circle 完成 Shape
impl Shape for Circle {
    fn area(&self) -> f64 {
        PI * self.radius * self.radius
    }
}
/// 为 Square 完成 Shape
impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}
/// 核算 trait 目标列表面积之和
fn get_total_area(shapes: Vec<Box<dyn Shape>>) -> f64 {
    shapes.into_iter().map(|s| s.area()).sum()
}
fn main() {
    let circle = Circle::new(5.0); // 完成了Shape trait 的结构体
    let shape: &dyn Shape = &circle; // &circle 是 trait 目标,用&dyn Shape申明
    println!("shape => {}", shape.area());
    // 特性目标也答应咱们在调集中存储不同类型的值:
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 1.0 }), // Box<Circle> cast to Box<dyn Shape>
        Box::new(Square { side: 1.0 }), // Box<Square> cast to Box<dyn Shape>
    ];
    assert_eq!(PI + 1.0, get_total_area(shapes)); // ✅
}

trait 目标履行动态分发

Rust能够同时支撑“静态分配(static dispatch)”和“动态分配(dynamic dispatch)”。

静态分发:指的是详细调用哪个函数,在编译阶段就确认下来了。Rust中的“静态分配”靠泛型(impl Trait是泛型的语法糖)来完成。对于不同的泛型类型参数,编译器会履行的单态化处理,为每一个被泛型类型参数替代的详细类型生成不同版别的函数和办法,在编译阶段就确认好了应该调用哪个函数。

动态分配:指的是详细调用哪个函数,在履行阶段才能确认。Rust中的“动态分配”靠 Trait Object 来完成。Trait Object 本质上是指针,它能够指向不同的类型,指向的详细类型不同,调用的办法也就不同。

例如:

trait Fly {
    fn fly(&self);
}
fn static_fly(fly: impl Fly) { // 静态分发,trait限制,履行单态化,编译阶段生成不同函数
    fly.fly()
}
fn static_fly1<T: Fly>(fly: T) { // 静态分发,泛型束缚,履行单态化,,编译阶段生成不同函数
    fly.fly()
}
fn dynamic_fly(fly: &dyn Fly) { // 动态分发,需求运用关键字 dyn,履行阶段才确认详细完成类型
    fly.fly()
}

当运用 trait 目标时,Rust 有必要运用动态分发。编译器无法知晓一切或许用于 trait 目标代码的类型,所以它也不知道应该调用哪个类型的哪个办法完成。为此,Rust 在运行时运用 trait 目标中的指针来知晓需求调用哪个办法。动态分发也阻止编译器有选择的内联办法代码,这会相应的禁用一些优化。

下面这张图很好的解释了静态分发Box<T>和动态分发Box<dyn Trait>的差异:

Rust 中的 trait 对象

trait 目标需求类型安全

只有目标安全(object-safe)的 trait 能够完成为特征目标。这里有一些复杂的规矩来完成 trait 的目标安全,但在实践中,假如一个 trait 中界说的都符合以下规矩,则该 trait 是目标安全的:

  • 办法的回来类型不能是Self
  • 办法没有任何泛型参数
  • Trait 不能Sized束缚。

这首要因为把一个目标转为 trait object 后,原始类型信息就丢失了,所以这里的 Self 也就无法确认了,那么该办法将不能运用原本的类型。当 trait 运用详细类型填充的泛型类型时也一样:详细类型成为完成 trait 的目标的一部分,当运用 trait 目标时其详细类型被抹去了,无法知道应该用什么类型来填充泛型类型。

一个非目标安全的 trait 例子是规范库中的Clonetrait。Clonetrait 中的clone办法的声明如下:

pub trait Clone {
    fn clone(&self) -> Self;
}

运用Box <dyn Clone>&dyn Clone 会报错

fn dynamic_clone(text: &dyn Clone) { // 报错: `Clone` cannot be made into an object
   text.clone()  
}

总结

假如说泛型给了咱们编译时的多态性,那么 trait 目标就给了咱们运行时的多态性。经过 trait 目标,咱们能够答应函数在运行时动态地回来不同的类型。trait 目标的结构体巨细是不知道的,所以有必要要经过指针来引证它们。详细类型与 trait 目标在字面上的差异在于,trait 目标有必要要用dyn关键字来修饰前缀。

参考

  • course.rs/basic/trait…
  • kaisery.github.io/trpl-zh-cn/…
  • rustwiki.org/zh-CN/refer…
  • rust-book.junmajinlong.com/ch11/04_tra…
  • github.com/pretzelhamm…
  • zhuanlan.zhihu.com/p/23791817
  • liujiacai.net/blog/2021/0…