Search CTRL + K

trait 对象的静态分发与动态分发

rust by example 是这么定义 trait 的 [1]

Traits

A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait.

自然,我们就会需要传递“实现了某个 trait”的 struct 这种范型能力。在 rust 中,提供了 两种方式 来实现这种能力,先引入一个 trait 和两个 struct 用于讲解后面的内容。

trait Run {
    fn run(&self);
}

struct Duck;

impl Run for Duck {
    fn run(&self) {
        println!("Duck is running");
    }
}

struct Dog;

impl Run for Dog {
    fn run(&self) {
        println!("Dog is running");
    }
}

静态分发和动态分发

首先引入分发 (dispatch):当代码涉及多态时,编译器需要某种机制去决定实际的调用关系。rust 提供了两种分发机制,分别是静态分发 (static dispatch) 和动态分发 (dynamic dispatch)。[2]

静态分发

静态分发其实就是编译期范型,所有静态分发在编译期间确定实际类型,Rustc 会通过单态化 (Monomorphization) 将泛型函数展开。

而静态分发有两种形式:

fn get_runnable<T>(runnable: T) where T: Run {
    runnable.run();
}

fn get_runnable(runnable: impl Run) {
    runnable.run();
}

两者在调用时都能通过

get_runnable(Dog {});

方式调用,区别在于前者可以使用 turbo-fish 语法(也就是 ::<> 操作符):

get_runnable::<Dog>(Dog {});

动态分发

首先引入 trait对象(trait object) 的概念:trait 对象是指实现了某组 traits 的非具体类型值,这组 trait 一定包含一个 对象安全(object safe) 的基 trait,和一些 自动trait(auto trait)

在 2021 版本后,要求 trait 对象一定需要 dyn 关键字标识,以和 trait 本身区分开来。对于某个 trait MyTrait,以下东西都是 trait 对象 [3]

动态分发也就是运行时范型,虽然 trait 对象是 Dynamically Sized Types(DST, 也叫unsized types),意味着它的大小只有运行时可以确定,意味着 rustc 不会允许这样的代码通过编译:

fn get_runnable(runnable: dyn Run) {
    runnable.run();
}

但是指向实现 trait 的 struct 的指针大小是一定的,因此可以把 trait 对象隐藏在指针后(如 &dyn TraitBox<dyn Trait>Rc<dyn Trait> 等),编译器编译时会默认对象实现了 trait,并在运行时动态加载调用的对应函数。

fn get_runnable(runnable: &dyn Run) {
    runnable.run();
}

动态分发靠的就是指向 trait 对象的指针。

实现原理

静态分发

静态分发的实现原理比较简单,每多一种调用类型,rustc 就会生成多一个函数:

fn get_runnable<T>(runnable: T) where T: Run {
    runnable.run();
}

fn main() {
    get_runnable::<Dog>(Dog {});
    get_runnable::<Duck>(Duck {});
}

通过编译后,get_runnable 函数会生成两种:

fn get_runnable_for_dog(runnable: Dog) {
    runnable.run()
}

fn get_runnable_for_duck(runnable: Duck) {
    runnable.run()
}

rustc 会自动将类型与调用函数匹配。

显而易见的,通过静态分发实现的多态无运行时性能损耗,但是编译出的二进制文件大小增加。

动态分发

动态分发就略复杂了,实现的关键在指针,每个指向 trait 对象的指针包含:

使用 trait 对象的目的是对方法的“延迟绑定(late binding)”,调用 trait 对象的某个方法最终在运行时才分发,也就是说调用时先从 vtable 中载入函数指针,再间接调用这个函数。对于 vtable 中每一个函数的实现,每个 trait 对象都可以不一样。

其实 rust 中 str 字符串类型和 [T] 数组类型都是 trait 对象。

对象安全

trait 对象一定要基于 对象安全 的 trait,这里不大谈特谈,只简单提及两个有趣的地方。


  1. https://doc.rust-lang.org/rust-by-example/trait.html ↩︎

  2. https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/trait-objects.html ↩︎

  3. https://doc.rust-lang.org/reference/types/trait-object.html ↩︎