Rust Object

rust有非常强大的类型系统。今天我们来说说rust的泛型。

rust有两种泛型:

  1. 基于static dispatch的泛型,类似于C++的模板。在编译期进行代码特化(monomorphization),为每一种类型生成一份代码。好处是执行效率高,但是会带来额外的冗余代码,使二进制文件变大(bloat)。

  2. 基于dynamic dispatch的泛型,类似于java和go的interface。在运行期查找虚表(vtable)来选择执行的方法。好处是使用灵活,但是性能肯定比static dispatch来的差。本篇着重介绍这一种泛型。


  • Trait Object

rust的dynamic dispatch实现都是基于一种叫做trait object的类型来实现的。先看一个例子:

    trait Object {
        fn dood(&self) -> int {
            1i
        }
    }
    impl Object for int {}
    impl Object for uint {}
    fn main() {
        fn gimme_an_object(i: &Object) {
            println!("{}", i.dood());
        }
        gimme_an_object(&2i);  // OUTPUT: 1
        gimme_an_object(&3u);  // OUTPUT: 1
    }

gimme_an_object函数这里发生了什么? 可以看到,gimme_an_object需要传入一个&Object类型的参数。就是说,gimme_an_object函数的参数i是一个实现了Object这个trait的引用类型。所以我们无论喂给它了一个&int或一个&uint,它都能完成调用。因为之前的两个impl已经为uint类型和int类型实现了Object这个trait。 在这一点上,rust的trait和go的interface很相似。我们只需要传入一个接口,函数就能完成工作,为不用管传入的参数到底是什么类型。 但是这里有一个细节需要注意:为什么要写&Object,写成fn gimme_an_object(i: Object)不行吗? 答案是不行。有人可能很奇怪,为什么我在go里面直接写interface就没问起,rust里面却必须要加个引用呢? 原因有两个:

  1. rust有三种原生指针,&、Box和*。无论哪一种都可以作为trait object的indirection,因此要是用interface一统江湖,不再写&,必然导致灵活性下降。无论用哪一种作为trait
    object的默认指针都有失偏颇。
  • trait object的编译器魔法。

在rust里,所有的指针都是一个字长。比如64位机器上,&1i的大小就是64个bit。 但是在trait object中,rust编译器会隐式的把指针转换为一个胖指针。

    // in core::raw::TraitObject
    struct TraitObject {
        data: *mut (),
        vtable: *mut (),
    }

也就是说,所有的TraitObject大小其实都是两个字长。第一个指向数据,第二个指向虚函数表。这点和go的interface其实是一模一样的。

  • trait safety

对于trait object,rust还有一个限制:只有safe的trait才能被用作trait object。 什么叫safe的trait呢? 因为有些trait会返回一个self类型,比如:

    trait RetSelf {
        fn ret_self(&self) -> Self;
    }

如果impl给了int,那么ret_self方法的返回值就是一个int,要是impl给了f64,那么返回值就是一个f64.这就意味着代码诸如:

    fn unsafe_object(i: &RetSelf) {
        let c = i.ret_self();
    }

是无法编译的,因为无从知道c的大小。因此在rust里面,只有不带有fn() -> Self类型的方法的trait才叫safe的trait,只有safe的trait才能被用作trait object。这也是为什么rust有很多trait xxxx, trait XXXXEXT。因为XXXX是safe的object,而trait XXXXEXT里面包含了带有返回Self的方法。如果把两者合并为同一个trait,意味着trait XXXX将不能再用于trait object。因此必须用两个trait来吧unsafe的方法隔离开。比如常用的Iterator trait就是如此。它从以前的一个trait变成了如今的interator和iteratorExt.

PhD Student in Computing Science

PhD Student @ SFU

Related