RUST 学习笔记 Enum Dispatch
最近在学习rust, 是我2022的年度目标之一. 开始归纳总结一些practise. 所以开了一个新坑.
Disclaimer: 学过的第一门语言是c, 然后就是java, python, go. 中途零星的用过cpp, 但是绝对是一团雾水的用, 所以对rust的了解, 都是来自一个对有gc语言比较熟悉的背景, 肯定会有很多理解不对的地方, 但是如果你有类似的背景, 也许能够帮到你. 所以, 如果有理解不对的地方, 欢迎斧正.
让我们先回顾一些基础知识
Trait
trait
是 rust 用来定义 shared behavior, 我就简单的理解成了interface.
我们接下来都用一个例子来讲解[1]
1trait Foo {
2 fn method(&self) -> String;
3}
4
5impl Foo for u8 {
6 fn method(&self) -> String { format!("u8: {}", *self) }
7}
8
9impl Foo for String {
10 fn method(&self) -> String { format!("string: {}", *self) }
11}
比如上述例子中, foo是一个shared behavior,我们为u8
和string
都做了实现.
Dispatch
dispatch
就是在要用调用一个interface的具体实现的方法的时候, 我们需要知道到底是哪个实例和它具体的类型, 因为有多个不同类型的实例可能实现了这个interface. 取决于dispatch的方式不同, 其成本也不同.
Static Dispatch
static dispatch
就是这个步骤在编译的时候我们就能够弄清楚.
在Rust中, 我们需要用trait bound, 就是说, 你需要在调用某个一个trait的方法时候, 告诉编译器这个trait具体是哪个trait.
1fn do_something<T: Foo>(x: T) {
2 x.method();
3}
4
5fn main() {
6 let x = 5u8;
7 let y = "Hello".to_string();
8
9 do_something(x);
10 do_something(y);
11}
比如 上述例子的 do_something
编译器就知道了, 这里的x得是一个实现了trait Foo
的类型.
在编译的时候, 因为我们知道, x
y
的类型, 1)我们可以check 他们是不是实现了trait Foo
, 2) 我们可以静态生产一些code, 在编译的时候就完成了这个dispatch.
比如上述的 do_something
方法就相当于为每一个具体的类型都生成了一个版本.
1fn do_something_u8(x: u8) {
2 x.method();
3}
4
5fn do_something_string(x: String) {
6 x.method();
7}
8
9fn main() {
10 let x = 5u8;
11 let y = "Hello".to_string();
12
13 do_something_u8(x);
14 do_something_string(y);
15}
这样在runtime的时候, dispatch的成本是很低的, 相当直接call了一个方法. Compiler还可以进一步的优化, 利用用inlining, 相当于在call这些方法的地方, 我们直接用这些方法的具体code来替换, 这样的成本就几乎为0了, 因为连method call都省掉了.
这里的劣势就是code bloat
, 代码会比较多, binary的size会变大.
这种情况下可以考虑 dynamic dispatch
Dynamic Dispatch
Rust中dynamic dispatch
是利用一个叫做 trait object
的东西来实现的.
比如这里的 &Foo
Box<Foo>
, 这里面的具体的类型只有到了runtime才能够决定.
可以简单的理解成 &Foo
Box<Foo>
是pointer, 但是要求这个pointer指向的实例的类型需要实现对应的trait
.
trait object 是没有类型的信息的, 这个技术也叫 type erasure
1fn do_something(x: &Foo) {
2 x.method();
3}
4
5fn main() {
6 let x = 5u8;
7 do_something(&x as &Foo);
8}
回到例子, 因为我们知道foo的定义, 所以我们是可以知道那些方法是可以被调用的, 只是我们在compile time的时候是不知道x
的具体类型的.
因此, dyanmic dispatch 虽然可以避免 code bloat
但是损失了一些优化的机会, 而且在runtime的时候需要 virtual function calls
, 因此就有一些成本了.
trait object rust 具体的实现
1pub struct TraitObject {
2 pub data: *mut (),
3 pub vtable: *mut (),
4}
一个trait object有一个data pointer和一个vtable pointer, data pointer指向个trait的data, 比如type T, 而 vtable pointer则指向vtable (virtual method table
), 里面存在 Foo对应type T的实现
vtable具体结构就是一个方法struct
1struct FooVtable {
2 destructor: fn(*mut ()),
3 size: usize,
4 align: usize,
5 method: fn(*const ()) -> String,
6}
7
8// u8:
9
10fn call_method_on_u8(x: *const ()) -> String {
11 // the compiler guarantees that this function is only called
12 // with `x` pointing to a u8
13 let byte: &u8 = unsafe { &*(x as *const u8) };
14
15 byte.method()
16}
17
18static Foo_for_u8_vtable: FooVtable = FooVtable {
19 destructor: /* compiler magic */,
20 size: 1,
21 align: 1,
22
23 // cast to a function pointer
24 method: call_method_on_u8 as fn(*const ()) -> String,
25};
26
27
28// String:
29
30fn call_method_on_String(x: *const ()) -> String {
31 // the compiler guarantees that this function is only called
32 // with `x` pointing to a String
33 let string: &String = unsafe { &*(x as *const String) };
34
35 string.method()
36}
37
38static Foo_for_String_vtable: FooVtable = FooVtable {
39 destructor: /* compiler magic */,
40 // values for a 64-bit computer, halve them for 32-bit ones
41 size: 24,
42 align: 8,
43
44 method: call_method_on_String as fn(*const ()) -> String,
45};
这样 trait_object.method()
就能调用对应的方法.
1let a: String = "foo".to_string();
2let x: u8 = 1;
3
4// let b: &Foo = &a;
5let b = TraitObject {
6 // store the data
7 data: &a,
8 // store the methods
9 vtable: &Foo_for_String_vtable
10};
11
12// let y: &Foo = x;
13let y = TraitObject {
14 // store the data
15 data: &x,
16 // store the methods
17 vtable: &Foo_for_u8_vtable
18};
19
20// b.method();
21(b.vtable.method)(b.data);
22
23// y.method();
24(y.vtable.method)(y.data);
这里我可能会等到理解更深刻的时候再revisit.
Enum Dispatch
Enum dispatch
是利用enum来代替trait object, 尤其是在有性能要求的情况下.
这个方法的原理其实就是static dispatch, 但是在语法是更便利, 相当于语法糖.
参见这个例子[2]
1trait KnobControl {
2 fn set_position(&mut self, value: f64);
3 fn get_value(&self) -> f64;
4}
5
6struct LinearKnob {
7 position: f64,
8}
9
10struct LogarithmicKnob {
11 position: f64,
12}
13
14impl KnobControl for LinearKnob {
15 fn set_position(&mut self, value: f64) {
16 self.position = value;
17 }
18
19 fn get_value(&self) -> f64 {
20 self.position
21 }
22}
23
24impl KnobControl for LogarithmicKnob {
25 fn set_position(&mut self, value: f64) {
26 self.position = value;
27 }
28
29 fn get_value(&self) -> f64 {
30 (self.position + 1.).log2()
31 }
32}
33
34fn main() {
35 // 这里使用 trait 对象
36 let v: Vec<Box<dyn KnobControl>> = vec![
37 //set the knobs
38 ];
39
40 //use the knobs
41}
1enum Knob {
2 Linear(LinearKnob),
3 Logarithmic(LogarithmicKnob),
4}
5
6impl KnobControl for Knob {
7 fn set_position(&mut self, value: f64) {
8 match self {
9 Knob::Linear(inner_knob) => inner_knob.set_position(value),
10 Knob::Logarithmic(inner_knob) => inner_knob.set_position(value),
11 }
12 }
13
14 fn get_value(&self) -> f64 {
15 match self {
16 Knob::Linear(inner_knob) => inner_knob.get_value(),
17 Knob::Logarithmic(inner_knob) => inner_knob.get_value(),
18 }
19 }
20}
类似, 因为写了不少boilterplate code, 所以维护性差一些,
可以利用 enum_dispatch
来自动生成相关的代码
代码会简洁很多.
1#[enum_dispatch]
2trait KnobControl {
3 //...
4}
5
6#[enum_dispatch(KnobControl)]
7enum Knob {
8 LinearKnob,
9 LogarithmicKnob,
10}