Rust Dispatch
学习的过程总是 无数个一知半解 + 一个恍然大悟的循环,两者的重要性难分伯仲。
分发(Dispatch)
当代码涉及到多态(polymorphism)时,需要机制判断最终执行的代码到底是哪一个具体的实现版本。这个过程就叫分发。
首先定义一点基础代码,作为后续内容的代码示例的默认前提:
trait Show {
fn print(&self);
}
impl Show for usize {
fn print(&self) {
println!("usize show: {:?}", &self);
}
}
impl Show for String {
fn print(&self) {
println!("string show: {:?}", &self);
}
}
静态分发(Static Dispatch)
泛型
类似于 C++ 里的模板,Rust 里的泛型(generic)包括加上 traits bounds 的泛型都是静态分发。具体实现是单态化(monomorphization):
In programming languages, monomorphization is a compile-time process where polymorphic functions are replaced by many monomorphic functions for each unique instantiation.
即代码在编译期间 多态方法 被多个单独的 单态方法 替换。
比如以下实现:
fn do_print<T>(x: T)
where
T: Show,
{
x.print();
}
fn main() {
let number = 42usize;
let string = "Dispatch".to_string();
do_print(number); // usize show: 42
do_print(string); // string show: "Dispatch"
}
实际会被展开成类似:
fn do_print_for_usize(x: usize) {
x.print();
}
fn do_print_for_string(x: String) {
x.print();
}
fn main() {
let number = 42usize;
let string = "Dispatch".to_string();
do_print_for_usize(number); // usize show: 42
do_print_for_string(string); // string show: "Dispatch"
}
通过编译期间的单态化,编译器去除了泛型的概念,优点是在运行期间无性能损耗,缺点是滥用泛型会导致生成的单态化代码变多,编译时间增加,生成的二进制文件体积变大。这和 C++ 模板是一致的。
impl Trait
除了泛型之外,impl Trait
也用作静态分发,impl Trait
可以用在参数类型和返回值类型里。比如上述的 fn do_print<T: Show>(x: T);
也可以用 impl Trait
的形式写成:
fn do_print(x: impl Show) {
x.print()
}
当 impl Trait
作为返回值类型时,需要注意编译器需要推导出返回值的具体类型来实现单态化,因此无法写出 if - else
等不同分支下返回不同类型(虽然它们都实现了这个 Trait )的代码。编译器无法推导出单态化的目标类型,一般会在后一种类型的返回处报错:expect A but get B...
实际编码中作为返回值写 impl Trait
的形式有两种用途:
- 让编译器推导类型,避免写特别长的类型
- 用在返回闭包的场景
第一个场景:看下面这个例子,实现了一个连接两个 Vector 的函数
fn combine_vec<T>(v: Vec<T>, u: Vec<T>) -> impl Iterator<Item = T>
where
T: Clone,
{
v.into_iter().chain(u.into_iter()).cycle()
}
fn main___() {
let v1 = vec![1, 2, 3];
let v2 = vec![5, 6, 7];
let mut v3 = combine_vec(v1, v2);
assert_eq!(Some(1), v3.next());
assert_eq!(Some(2), v3.next());
assert_eq!(Some(3), v3.next());
assert_eq!(Some(5), v3.next());
assert_eq!(Some(6), v3.next());
assert_eq!(Some(7), v3.next());
assert_eq!(Some(1), v3.next());
}
泛型函数 combine_vec
的真实返回值是 std::iter::Cycle<std::iter::Chain<std::vec::IntoIter<T>, std::vec::IntoIter<T>>>
,这里编译的时候会推导出返回值的类型和泛型 T
的类型,实际上会生成类似如下代码:
fn combine_vec_for_i32_with_explicit_return_type(
v: Vec<i32>,
u: Vec<i32>,
) -> std::iter::Cycle<std::iter::Chain<std::vec::IntoIter<i32>, std::vec::IntoIter<i32>>> {
v.into_iter().chain(u.into_iter()).cycle()
}
很明显我们的这个函数的意图是利用迭代器的 chain
方法连接两个 Vec
,因此返回的类型一定还是原先泛型 T
的迭代器,因此简单的写上 impl Iterator<...>
,剩下的工作让编译器去做就好了。
因为即使我们写成确切的类型,也不会给对读代码的人提供更多有帮助的信息,徒增阅读负担而已。
第二个场景:Rust 里闭包类型是匿名的,无法显示的写出来,这种情况下我们只能只用 impl Trait
的形式来写:
fn adder_function(y: i32) -> impl Fn(i32) -> i32 {
move |x: i32| x + y
}
fn double_positive<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a {
numbers.iter().filter(|x| x > &&0).map(|x| x * 2)
}
fn main() {
let add_one = adder_function(1);
assert_eq!(3, add_one(2));
let v = vec![-3, 2, -4, 1];
let v2 = double_positive(&v).collect::<Vec<i32>>();
assert_eq!(v2, vec![4, 2]);
}
动态分发(Dynamic Dispatch)
静态分发的缺点:无法返回多种类型 正是动态分发要解决的问题。使用 Trait Object 表达 实现了某种 Trait 的类型(的集合) 这种类型,这种类型有点像 OOP 语言里的基类/抽象类,本身无法实例化出对象。
Trait Object
Trait Object 本身可以理解为有固定大小的类型,其包含两个指针,一个指向其实际的类型,一个指向实现了 TraitObject 的这个 Trait 的虚表,可以理解为如下表达:
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
虽然 Trait Object 大小是确定,但是并不能写出形如 fn x() -> Trait Object
的代码,你会得到编译器的警告:
return type cannot have an unboxed trait object, doesn't have a size known at compile-time
嗯?这是因为 Trait Object 所表达的实现了该 Trait 的类型的集合,其中的元素的大小是不确定的。Trait Object 本身是无法实例化的,作为返回值自然是没有意义的,真正要作为返回值的是某个实现了该 Trait 的类型的实例。因此要明确并不是因为 Trait Object 本身大小不固定,大小就是宽指针是固定的。
dyn Trait
明白了 Trait Object 的概念,代码中要表示一个 Trait Object,需要使用 dyn
关键字:dyn SomeTrait
表示 SomeTrait
的 Trait Object
这里有些历史,在 2016 年之前还没有 dyn
这个关键字,RFC-2113 中引入了这个关键字语法,并在 rust 1.26/2021 edition 后必须使用 dyn
才能表示 Trait Object
要求使用 dyn
就是为了清晰表达含义,区分 Trait 和 TraitObject。提案里给出了几个实例:比如下面代码:
impl SomeTrait for AnotherTrait {...}
impl SomeTrait {...}
这在引入 dyn 关键字之前都是合法的代码(现在会提示了),能分清楚这里的几个 xxxTrait,哪个是 Trait ,哪个是 Trait Object 么 :)
第一条按照 impl trait for type
的语法,SomeTrait
是 Trait, AnotherTrait
应该是 Trait Object;
第二条很容易理解为给 SomeTrait
实现一些默认方法,但是应该在定义处 trait SomeTrait {...}
里实现默认方法,这里其实是表达给 SomeTrait
的 Trait Object 实现方法。
当然如今这样写已经会被编译器警告了,正确的写法是:
impl SomeTrait for dyn AnotherTrait {...}
impl dyn SomeTrait {...}
拓展知识: Trait Object 都是 unsized ,所以 Trait 在设计的时候都是默认
?Sized
来支持 Trait Object:Trait Object Sizeness因此如果定义
trait SomeTrait : Sized
,那么这个 Trait 就不能使用 Trait Object 了。换句话说,Trait Object 要求不能包含Sized
Bounds。
- All supertraits must also be object safe.
Sized
must not be a supertrait. In other words, it must not requireSelf: Sized
.
vtable in Rust
首先先看一下 C++ 里的虚表实现,对于一个子类对象,其内存布局包括:(注:这里只考虑继承包含虚函数父类的子类的情况,仅为和 Rust 虚表对比,不能完全代表真实内存布局实现)
子类内存布局 | vtable | |
---|---|---|
虚表指针 | —> | 析构方法指针 |
子类成员/数据 | 成员方法指针 |
指向虚表的指针和自身成员数据。
对于多继承的情况,会有多个虚表指针:
vtable | 子类内存布局 | vtable | ||
---|---|---|---|---|
虚表指针 | —> | 析构方法指针 | ||
析构方法指针 | <— | 虚表指针 | 成员方法指针 | |
成员方法指针 | 子类成员/数据 |
而 Rust 里,如上 TraitObject 的 raw code:很容易得出单 Trait 下的 TraitObject 布局:
子类布局 | Trait Object | vtable | ||
---|---|---|---|---|
成员数据 | <— | 数据指针 | ||
虚表指针 | —> | 析构方法指针 | ||
成员方法指针 |
那么如果我想写出多个 Trait Bounds 的 Trait Object 呢?如下代码:
trait FirstTrait {
fn first(&self);
}
trait SecondTrait {
fn second(&self);
}
struct Subject {
subjects: dyn FirstTrait + SecondTrait, // error: only auto traits can be used
// as additional traits in a trait object
}
会在 + SecondTrait
处得到如上提示,以及如下建议:
consider creating a new trait with all of these as supertraits and using that trait here instead:
trait NewTrait: FirstTrait + SecondTrait {}
(rustc 真贴心地教你写代码)按照提示需要这样写:
trait AllTrait: FirstTrait + SecondTrait {}
struct Subject {
subjects: dyn AllTrait,
}
很容易猜测到这是因为 TraitObject 的实现方式:仅包含两个指针,无法增加更多的 vtable 指针来达成类似 C++ : public A, public B
多继承的效果。
拓展知识:目前 supertrait 下还不支持 upcasting coercion: issues
fn need_first_trait(o: &dyn FirstTrait) {}
impl Subject {
fn call_as_first_trait(&self) {
need_first_trait(&self.subjects) // error: cannot cast `dyn AllTrait` to `dyn FirstTrait`,
// trait upcasting coercion is experimental
}
}
目前编译器还无法识别出应该放入哪个 FirstTrait
的 Trait Object 的虚表,可以用下面的方式手动补充实现:
trait AllTrait: FirstTrait + SecondTrait {
fn as_first_trait(&self) -> &dyn FirstTrait;
}
impl<T: FirstTrait + SecondTrait> AllTrait for T {
fn as_first_trait(&self) -> &dyn FirstTrait {
self
}
}
impl Subject {
fn call_as_first_trait(&self) {
need_first_trait(self.subjects.as_first_trait())
}
}