RUST 学习笔记 Enum Dispatch

9 minute read

最近在学习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,我们为u8string都做了实现.

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}

Reference

  1. https://doc.rust-lang.org/1.8.0/book/trait-objects.html
  2. https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/safe-guides/coding_practice/traits/trait-object.html