泛型
函数泛型
1 | fn foo<T>(arg: &T) -> &T { |
结构体泛型
1 | struct Point<T> { |
枚举泛型
1 | enum Option<T> { |
方法泛型
1 | struct Point<T> { |
const 泛型
const 泛型是使用值而不是类型作为泛型参数,需要指定值的类型
1 | // 一个N维坐标点类型 |
作为函数的泛型参数时,可以作为常量用于函数体中
1 | // const泛型参数可以用于函数中 |
const 泛型参数可以传入一个常量表达式,使用大括号包围
1 | let arr = [1, 2, 3]; |
常量函数
使用 const
修饰一个函数,表示它是常量函数,常量函数会在编译期执行,将计算的常量值结果在调用处替换
1 | fn foo<const N: usize>(arr: &[i32; N]) { |
默认泛型参数
泛型参数定义时可以使用默认值
1 | trait Add<RHS=Self> { |
泛型的性能
rust 的泛型与 java 不同,java 仅在编译期检查类型,之后进行类型擦除,在运行时会丢失类型信息,而 rust 在编译时,会生成不同具体类型的代码,再将泛型定义替换为具体定义,避免了类型擦除问题,这称为单态化
例如,使用 Option 枚举,单态化会为每种具体类型生成具体代码
1 | let integer = Some(5); |
Trait
一个类型的行为由其可供调用的方法构成,如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了,Trait 是一种包含多个方法的集合,类似于接口
1 | pub trait Summary { |
为类型实现 Trait,只有当 Trait 和类型至少有一方位于本地 crate 时,才能实现 Trait
1 | pub struct NewsArticle |
Trait 可以有默认实现
1 | pub trait Summary { |
newtype 模式
newtype 模式用于为外部类型实现外部 Trait,例如 Vec<T>
类型没有实现 Display
特征,而二者都是从外部引入的,要为 Vec<T>
实现 Display
特征就需要使用 newtype 模式
具体步骤如下
- 为外部类型定义一个元组结构体
- 为元组结构体实现 Trait
示例如下
1 | use std::fmt; |
Trait 约束
可以将函数的参数指定为实现了某个 Trait 的类型
1 | pub trait Summary { |
以上实际是 Trait 约束(Trait Bound)的语法糖
1 | // 限制必须是实现Summary的同一个类型 |
在实现方法时指定 Trait 约束可以有条件地实现方法
1 | struct Pair<T> { |
多个泛型参数的语法糖
使用 +
指定多个 Trait
1 | // 参数需要实现Summary和Display两个Trait |
使用 where
关键字简化 Trait 约束
1 | fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { |
Trait 对象
当在返回值类型上使用 Trait 约束时,rust 不允许返回不同类型的对象,即使它们满足 Trait 约束
例如下面的代码,编译无法通过
1 | fn returns_summarizable(switch: bool) -> impl Summary { |
编译无法通过是因为 rust 无法在编译期确定返回值类型,函数可能返回 Post 类型,也可能返回 Weibo 类型,只能在运行时确定
当代码中引入多态时,需要一种机制确定实际调用的类型,这称为分发,分发分为静态分发和动态分发
- 静态分发:在编译期确定实际类型
- 动态分发:在运行时确定实际类型
rust 默认使用单态化生成具体类型代码,在编译期就确定了实际类型,即实现了静态分发。但对于上述代码无法在编译期确定实际类型,只能在运行时确定实际类型的情况,就需要使用动态分发
从类型的角度看,一个 Trait 是包含了具有某种特性的类型的集合,Trait 本身也可以看做一个类型,将 Trait 作为类型的特性在 rust 中通过 Trait 对象(Trait Object)实现,同时 rust 通过 Trait 对象实现了动态分发
例如,通过 Trait 对象在 Vec 中保存不同的类型,定义以下 Trait 和类型
1 | trait A {} |
现在我们需要在一个 Vec 中同时保存 AImpl1
和 AImpl2
,如果我们将 Trait 当做 java 中的接口,我们会很自然地写出如下代码
1 | let v: Vec<&A> = vec![&AImpl1, &AImpl2]; |
这段代码会被编译器报错,因为这里没有使用 Trait 对象,使用 Trait 对象需要加上 dyn
关键字,以下代码可以编译通过
1 | let v: Vec<&dyn A> = vec![&AImpl1, &AImpl2]; |
这里指定 Vec 的泛型时,我们将 A
看做了一个类型,而 Trait 本身并不是类型,此时就需要 Trait 对象来实现 Trait 的类型特性(私以为可以将 Trait 对象理解为编译时类型,实现了 Trait 的实际类型为运行时类型)
实现多态的效果
通过 Trait 对象,可以实现多态中父类类型引用子类对象的效果
1 | trait A { |
以上代码类似下面的 java 代码
1 | interface A { |
当我们使用 Trait 对象时,也就使用了动态分发,编译器会在运行时确定 Trait 的实际类型,下面的代码可以编译通过
1 | struct BImpl1; |
Trait 对象是动态大小类型
需要注意的是,Trait 对象本身是动态大小类型,无法在编译期确定大小,因此,dyn Trait
不能作为值类型使用,而 Trait 对象的引用的大小是固定的,因此需要通过引用或者智能指针来使用 Trait 对象,下面的代码无法编译通过
1 | // 无法编译通过 |
Trait 对象的限制
不是所有 Trait 都拥有 Trait 对象,只有满足对象安全的 Trait 才能使用 Trait 对象,对象安全要求 Trait 的所有方法都满足以下条件
-
方法参数必须包含
self
或Self
且不能使用self
或Self
的值类型在类型 T 实现 Trait 时,
self
和Self
会转换为实际类型,使用self
或Self
的值类型时,表示实际类型的实例需要移动或拷贝,而使用 Trait 对象时,无法确定在编译期确定实际类型,也就无法确定实际类型数据应该进行移动还是拷贝,下面的代码无法编译通过1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16trait A {
fn a(self);
}
struct AImpl1;
impl A for AImpl1 {
fn a(self) {
println!("a1");
}
}
fn main() {
let mut a: &dyn A = &AImpl1;
a.a(); // 不合法,编译期无法确定a的实际类型,无法确定a方法中实例应该移动还是拷贝
} -
Self
不能用于方法的返回值使用 Trait 对象时,无法在编译期确定类型,不能保证参数中的
Self
和返回值类型Self
是同一个类型1
2
3
4trait A {
// 两个位置的Self不能在编译期保证是相同的类型
fn a(&self) -> &Self;
} -
方法不能使用泛型参数
方法中使用泛型参数,编译器会在编译期对实际类型进行泛型单态化,而使用 Trait 对象时,无法在编译期确定实际类型,因此也就无法进行单态化。若在每个实际类型的虚表中记录所有泛型的具体实现,则会造成极大的开销。
动态分发
Trait 对象的动态分发通过虚表(vtable)机制实现,Trait 对象的引用是一个胖指针,其中存储了两个指针,占两个指针的大小
- data 指针:指向实际类型数据的指针
- vtable 指针:指向虚表的指针,其中包含了所有动态分发的方法
下图展示了 Box<T>
和 Box<dyn Trait>
的区别,Box<T>
只有一个指向堆内存中的数据的指针 ptr,Box<dyn Trait>
中的 ptr(data 指针)指向堆内存中的实际类型数据,vptr(vtable 指针)指向编译器为 T 类型生成的 Trait 虚表

虚表的基本布局
rust 中的虚表可以分为 header 和 entry 两个部分
-
header
虚表都包含一个 header,其中包含三个
usize
大小的字段,分别是drop_in_place
、size
和align
drop_in_place
:指向销毁函数的指针size
:实际类型对象的大小align
:实际类型对象的内存对齐值
通过以上三个字段,使得 Trait 对象可以被销毁和释放。当销毁 Trait 对象时,首先调用
drop_in_place
指向的函数,销毁实际类型对象,之后将size
和align
传入dealloc
函数中释放堆内存 -
entry
entry 部分保存了实际类型实现的 Trait 方法的地址
例如,T 类型实现了 Trait,编译器为 T 类型生成的 Trait 虚表结构如下所示
1
2
3
4
5trait Trait {
fn fun1(&self);
fn fun2(&self);
fn fun3(&self);
}1
2
3
4
5
6
7
8
9
10
11
12
13+--------------------------+
| fn drop_in_place(*mut T) | --> drop_in_place
+--------------------------+
| size of T | --> size
+--------------------------+
| align of T | --> align
+--------------------------+
| fn <T as Trait>::fun1 | --> fun1具体实现的地址
+--------------------------+
| fn <T as Trait>::fun2 | --> fun2具体实现的地址
+--------------------------+
| fn <T as Trait>::fun3 | --> fun3具体实现的地址
+--------------------------+
动态分发的工作流程
以下面的代码为例
1 | trait A { |
在编译期,编译器为 AImpl1
和 AImpl2
分别生成一张 A 的虚表,其中分别包含了 AImpl1
和 AImpl2
的 header 信息和实现的 a 方法的地址
在运行时
let mut a: &dyn A = &AImpl1;
:a 中的 data 指针指向AImpl1
的实例,vtable 指针指向AImpl1
的虚表a.a();
:调用AImpl1
虚表中的 a 方法a = &AImpl2;
:a 中的 data 指针指向AImpl2
的实例,vtable 指针指向AImpl2
的虚表a.a();
:调用AImpl2
虚表中的 a 方法
关联类型
在定义 Trait 时,使用 type
关键字定义一个自定义类型,可以在定义特征时简化泛型
1 | // 若使用泛型,则需要写成Iterator<T>,使用关联类型,避免添加过多泛型 |
关联类型也可以添加 Trait 约束
1 | trait CacheableItem { |
同名方法调用
当实现类型与 Trait 出现了同名方法时,rust优先调用实现类型的方法
调用特征中的方法
1 | fn main() { |
调用特征中的关联函数
1 | trait Animal { |
Trait 定义约束
在定义 Trait 时,可以增加对 Trait 的约束,实现该 Trait 需要实现类型满足相应的约束
1 | use std::fmt::Display; |