美文网首页
Rust Trait

Rust Trait

作者: 黑天鹅学院 | 来源:发表于2021-11-13 10:25 被阅读0次

    观感

    Rust的Trait和Golang的interface看起来非常相似,从开发者角度来看,都可以实现具体类型的抽象化。

    golang:

    type geometry interface {
        area() float64
    }
    
    type rect struct {
        width, height float64
    }
    
    func (r rect) area() float64 {
        return r.width * r.height
    }
    
    func measure(g geometry) {
        fmt.Println(g)
        fmt.Println(g.area())
    }
    
    func main() {
        r := rect{width: 3, height: 4}
        measure(r)
    }
    

    Rust:

    use core::f64::consts::PI;
    use core::fmt::Debug;
    
    trait Geometry {
        fn area(&self) -> f64;
    }
    
    #[derive(Debug)]
    struct Rect {
        width: f64,
        height: f64,
    }
    
    impl Geometry for Rect {
        fn area(&self) -> f64 {
            self.width * self.height
        }
    }
    
    fn main() {
        fn measure<T>(g: &T)
        where T: Geometry + Debug 
        {
            println!("{:?}", g);
            println!("{:?}", g.area());
        }
        
        let r = Rect{width: 3.0, height: 4.0};
        
        measure(&r);
    }
    

    从上面的代码可以简单看出来,Golang中的Interface与具体的结构体之间是自动关联的,不像Rust需要显式的用一个impl来关联。

    此外,回顾下前文范型相关的内容看,Rust可以为非确定类型实现trait,但是Golang仅能对确定的struct实现Interface。

    pub trait MetroCodeCheck {
        fn metro_status(&self) -> String;
    }
    
    impl<T> MetroCodeCheck for T
    where
        T: TravelCodeCheck,
    {
        fn metro_status(&self) -> String {
            format!("{}", self.travel_status())
        }
    }
    

    在这个例子中,为T类型实现了MetroCodeCheck,而T是一个范型,可能对应于其他的已定义的类型,并不与一个确切的类型绑定。

    静态分发

    下面我们通过模拟编译器的行为来分别分析静态分发。对于Golang而言,仅允许动态分发,每一个Interface中的方法地址是从值中动态加载然后调用的,所以只有在运行期间才能知道具体的函数。

    考虑一个例子:

    type Foo interface { bar() }
    
    func call_bar(value Foo) { value.bar() }
    
    type X int;
    type Y string;
    func (X) bar() {}
    func (Y) bar() {}
    
    func main() {
        call_bar(X(1))
        call_bar(Y("foo"))
    }
    

    如果用C语言模拟Golang的原理,忽略掉一些必要的细节后,会得到类似的代码:

    void bar_int(...) { ... }
    void bar_string(...) { ... }
    
    struct Foo {
        void* data;
        struct FooVTable* vtable;
    }
    struct FooVTable {
        void (*bar)(void*);
    }
    
    void call_bar(struct Foo value) {
        value.vtable.bar(value.data);
    }
    
    static struct FooVTable int_vtable = { bar_int };
    static struct FooVTable string_vtable = { bar_string };
    
    int main() {
        int* i = malloc(sizeof *i);
        *i = 1;
        struct Foo int_data = { i, &int_vtable };
        call_bar(int_data);
    
        string* s = malloc(sizeof *s);
        *s = "abc";
        struct Foo string_data = { s, &string_vtable };
        call_bar(string_data);
    }
    

    可以看出Interface中的函数的地址保存在vtable中,在调用过程中,必须先根找到对应的vtable才能获取到函数地址。

    如果是Rust,会得到如下的C代码:

    void bar_int(...) { ... }
    void bar_string(...) { ... }
    
    void call_bar_int(int value) {
        bar_int(value);
    }
    void call_bar_string(string value) {
        bar_string(value);
    }
    
    int main() {
        call_bar_int(1);
        call_bar_string("abc");
        return 1;
    }
    

    Rust直接在编译阶段为不同的类型生成了不同的函数版本,然后直接根据类型调用不同的版本即可,不涉及到从vtable获取函数地址。

    从调用过程可以直接看出,静态分发模式下,省去了动态查找,也可以做一些更加深层次的优化,导致的结果就是Rust比Golang效率更高,但是可能会导致代码膨胀。

    动态分发

    Rust同时支持静态分发与动态分发,看一个实际的例子。

    trait Animal {
        fn speak(&self);
    }
    struct Dog;
    impl Animal for Dog {
        fn speak(&self) {
            println!("旺旺.....");
        }
    }
    struct Cat;
    impl Animal for Cat {
        fn speak(&self) {
            println!("喵喵.....");
        }
    }
    

    如果是采用静态分发,那么使用方法如下:

    fn animal_speak<T: Animal>(animal: T) {
        animal.speak();
    }
    
    fn main() {
        let dog = Dog;
        let cat = Cat;
    
        animal_speak(dog);
        animal_speak(cat);
    }
    

    实际上相当于为DogCat分别实现了animal_speak方法:

    fn dog_speak(dog: dog) {
        dog.speak();
    }
    
    fn cat_speak(cat: Cat) {
        cat.speak();
    }
    

    如果是动态分发,那么使用方法如下:

    fn animal_speak(animal: &dyn Animal) {
        animal.speak();
    }
    
    fn main() {
        let dog = Dog;
        let cat = Cat;
    
        animal_speak(dog);
        animal_speak(cat);
    }
    

    这里使用了dyn作为动态分发的标记。

    总结

    Rust trait同时支持静态分发与动态分发,静态分发不需要通过虚表来寻找实际需要的函数指针,而是直接获取了函数指针,中间少了一步寻址过程。

    从性能角度看,动态分发会带来运行时开销,静态分发性能更好,但是可能会造成二进制文件膨胀。

    相关文章

      网友评论

          本文标题:Rust Trait

          本文链接:https://www.haomeiwen.com/subject/tdrmiltx.html