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++中的虚函数表
- 虚函数表(Virtual Function Table)是一个存放类中所有虚函数地址的表
- 通过基类指针或引用调用虚函数时,程序会根据对象的实际类型查找对应类的虚函数表,并调用正确的虚函数。
- 当一个包含虚函数的类被实例化时,虚函数表也被分配到该实例的内存中。
- 对象中包含一个指向虚函数表的指针(通常位于对象内存布局的最前面)。
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++模板元编程展开会很大很复杂,如有必要,后续会将开专题继续讨论。
如有勘误,敬请指出。
(这里还有一些细节没有讨论到,后续会结合实际应用具体讨论)