【Rust中的泛型与特征(二)】


前言

Rust中的泛型与特征(一)中,我们整理了rust泛型,特征trait,特征约束,以及与其他语言比较下的异同,本文重点结合前述知识点,与C++语言特性相比较,方便理解,重点在Rust的特征对象


Rust中的泛型与特征(一)提到:trait可作为函数返回值使用

考虑如下代码:

//声明一个特征
pub trait Noise {
    
    
    fn noise(&self) -> String;
}

//声明一个小狗结构体类型,一个小猫结构体类型,并分别为其类型实现Noise特征
pub struct Dog {
    
    
    pub voice: String,
}

impl Noise for Dog {
    
    
    fn noise(&self) -> String {
    
    
        format!("小狗的叫声{}", self.voice)
    }
}

pub struct Cat {
    
    
    pub voice: String,
}

impl Noise for Cat {
    
    
    fn noise(&self) -> String {
    
    
        format!("小猫的叫声{}", self.voice)
    }
}
//请问,这个函数是否有问题?
fn returns_noise(isdog: bool) -> impl Noise {
    
    
    if isdog {
    
    
        Dog {
    
    
            voice: String::from(
                "wangwang",
            ),
        }
    } else {
    
    
        Cat {
    
    
            voice: String::from(
                "miaomiao",
            ),
        }
    }
}

结论是:return_noise函数无法通过编译,因为trait作为返回值类型无法直接返回不同的数据类型。
有“问到底”的同学会想:为什么不可以呢,这显然符合逻辑!
来看看官方给我们的解释:

The Rust compiler needs to know how much space every function’s return type requires. This means all your functions have to return a concrete type. Unlike other languages, if you have a trait like Animal, you can’t write a function that returns Animal, because its different implementations will need different amounts of memory.
Rust编译器需要知道每一个函数返回类型具体需要多大的空间,这意味着你的函数必须返回一个确切的类型不同于其他类型的语言,如果你有一个trait (Noise),你不能直接返回一个Noise,因为它的(triats)的不同实现需要的内存量是不同的。

如何解决:使用特征对象

Rust的特征对象定义

特征对象 指向 实现了某种特征的类型的 实例,其映射关系存储在一张表中,在运行时可以通过特征对象找到特定的调用方法。
同样先看看官方给我们的解释:
Instead of returning a trait object directly, our functions return a Box which contains some Animal. A box is just a reference to some memory in the heap. Because a reference has a statically-known size, and the compiler can guarantee it points to a heap-allocated Animal, we can return a trait from our function!
Rust tries to be as explicit as possible whenever it allocates memory on the heap. So if your function returns a pointer-to-trait-on-heap in this way, you need to write the return type with the dyn keyword, e.g. Box< dyn Animal >.
结合示例,我将把官方提到的Animal在翻译时全都替换成Noise
为了代替 返回实现来了某种特定trait的对象,我们的方法使用一个"Box< dyn Noise>" ,【不知道Box的不要紧,可以直接理解成这就是指向堆内存的父类指针,也可以看作将子类对象"装箱"】,Box就是一个堆内存上的引用,因为Rust引用是已知的固定大小,编译器就可以保证这个Box返回值是指向了一个堆上的Noise引用,【Rust十分注重内存安全,所以如果你使用了pointer-to-trait-on-heap 这种方式,你就需要在Box声明类型是加上dyn】【dyn 即dynamic】

官方的解释已经足够明确了,来看看改造后正确可以运行的代码的样子:

fn returns_noise(isdog: bool) -> Box<dyn Noise> {
    
    
    if isdog {
    
    
        Box::new(Dog {
    
    
            voice: String::from(
                "wangwang",
            ),
        })
    } else {
    
    
        Box::new(Cat {
    
    
            voice: String::from(
                "miaomiao",
            ),
        })
    }
}

C++中的动态多态

C++中的虚函数表

  1. 虚函数表(Virtual Function Table)是一个存放类中所有虚函数地址的表
  2. 通过基类指针或引用调用虚函数时,程序会根据对象的实际类型查找对应类的虚函数表,并调用正确的虚函数。
  3. 当一个包含虚函数的类被实例化时,虚函数表也被分配到该实例的内存中。
  4. 对象中包含一个指向虚函数表的指针(通常位于对象内存布局的最前面)。
    虚函数表在C++中的使用

Rust中的动态分发(图片引用自《Rust语言圣经》)

在这里插入图片描述
这里我们主要关注Box ,可以看到一个指针指向了堆上的具体数据类型,一个vptr指向了Box维护的vtable,实际上就是虚函数表,再通过表中维护的映射关系,进一步找到并执行所需要函数。

不论是C++中的动态多态,亦或动态绑定,还是Rust中的动态分发,其原理都是借助指代关系【c++的指针或是Rust中的Box引用,或是C#中的装箱】,来统一类型,实现复用和”美观“,最后通过虚函数表的映射关系来找到真正对应关系的函数执行,极大范围内提升了代码的灵活性。

Rust中的Self和self

代码示例:

trait Something {
    
    
	fn something(&self)->Self;
}

通过上述例子可以看的出来,self相当于C++中的this指针,而Self则是实际上this的类型

编译期计算

C++模板元编程中的编译期计算

#include <iostream>
#include <type_traits>

template<int A, int B>
struct Sum {
    
    
    static const int value = A + B; 
};

Rust泛型编程中的编译期计算

struct Sum<const A: usize, const B: usize>;
impl<const A: usize, const B: usize> Sum<A, B> {
    
    
    const VALUE: usize = A + B;
    fn get_value(&self) -> usize {
    
    
        Self::VALUE
    }
}

由上述两个例子可以看得出来,C++ 和Rust都支持编译期计算,节省了运行时的开销,同时它们计算结果也都存储在了程序确定的内存位置(编译期常量),如Rust的const,C++的static const,这其实就是所谓的函数式编程范式,所有的内存使用在编译期间都是可知并且固定的。

Ps:C++模板元编程展开会很大很复杂,如有必要,后续会将开专题继续讨论。

如有勘误,敬请指出。
(这里还有一些细节没有讨论到,后续会结合实际应用具体讨论)