深入 trait
Rust 中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象,均基于 trait 来完成。
trait 有以下4种用法:
- 接口抽象: 接口是对类型行为的统一约束。
- 泛型约束: 泛型的行为被 trait 限定在更有限的范围内。
- 抽象类型: 在运行时作为一种间接的抽象类型去使用,动态地分发给具体的类型。
- 标签 trait: 对类型的约束,可以直接作为一种 “标签” 使用。
1. 接口抽象
trait 最基础的用法就是进行接口抽象。
-
使用 trait 关键字来定义接口。
-
使用 impl 关键字为类型实现接口方法。
-
接口中可以定义方法,并支持默认实现。
-
接口中不能实现另一个接口,但是接口之间可以继承。
-
同一个接口可以同时被多个类型实现,但不能被同一个类型实现多次。
1.1 关联类型
Rust 标准库 关于 Add trait 的定义:
pub trait Add<Rhs = Self> {
/// The resulting type after applying the `+` operator.
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
内部使用 type 定义 Output 类型,以这种方式定义的类型叫做 关联类型。
Add<Rhs = Self> 这种形式表示 为类型参数 RHS 指定了 默认值 Self。
Self 是每个 trait 都带有的 隐式类型参数,代表实现当前 trait 的具体类型。
在语义层面上,使用关联类型也增强了 trait 表示行为的这种语义,因为它表示了和某个行为(trait)相关联的类型。在工程上,也体现出了 高内聚 的特点。
在实现 trait 的时候,关联类型 Output 必须指定具体类型。
1.2 trait 一致性
以下代码编译会出错
// conflicting implementations of trait `std::ops::Add` for type `u64`
// conflicting implementation in crate `core`:
// only traits defined in the current crate can be implemented for arbitrary types
// define and implement a trait or new type insteadrustc[E0117]
use std::ops::Add;
impl Add<u64> for u64 {
type Output = u64;
fn add(self, rhs: u64) -> Self::Output {
self + rhs
}
}
trait 的孤儿规则 : 如果要实现某个 trait, 那么该 trait 和 要实现该 trait 的那个类型至少有一个要在当前的 crate 中定义。
- 目的是禁止改写其他 crate 中某类型对trait的实现。
1.3 trait 继承
Rust 不支持传统面向对象的继承,但是支持 trait 继承。
子 trait 可以继承 父 trait 中 定义或实现 的方法。
trait A {
fn aa(&self, i: i32) {
println!("aa = {}", i);
}
}
trait B {
fn bb(&self, i: i32) {
println!("bb = {}", i);
}
}
struct R;
impl A for R {}
impl B for R {}
struct S;
impl A for S {}
impl B for S {}
// 这里是 trait C 继承 了 trait A 和 trait B。
// trait 后面的 冒号 表示继承,加号 表示 继承 A 和 B。
trait C: A + B {
fn cc(&self, i: i32) {
println!("cc = {}", i);
}
}
// impl C for R {}
// impl C for S {}
// 此处表示,为所有 拥有(实现) trait A 和 B 行为的类型实现 trait C。等价于上面的“C 分别实现 R 和 S”。
impl<T: A + B> C for T {}
fn main() {
let r = R {};
r.aa(1);
r.bb(2);
r.cc(3);
let s = S {};
s.aa(7);
s.bb(8);
s.cc(9);
}
2. 作为泛型约束
使用泛型编程时,绝大多数情况下的行为并不是针对所有类型都实现的,而是对泛型类型有 trait 约束的。
- 如果是任意类型,能有多少共同操作呢?
- 限定了 trait 类型,才能调用 泛型类型 对应的 trait 方法。
2.1 trait 限定
use std::ops::Add;
// 只有实现了 Add trait 的类型,才能使用 + 进行相加操作
fn my_sum<T: Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("my_sum(1i32, 2i32) = {}", my_sum(1i32, 2i32));
println!("my_sum(1u16, 2u16) = {}", my_sum(1u16, 2u16));
}
使用 trait 对泛型进行约束,叫做 trait 限定 (trait Bound)。基本格式为:
fn fn_name<T: MyTrait + MyOtherTrait + SomeStandardTrait>(t: T) {
xxx
}
2.2 理解 trait 限定
-
从数学的角度来看待: 类型可以看作具有相同属性值的集合。
-
trait 也可以看作一种类型,一种方法集合,或者一种行为的集合。
-
impl<T : A + B> C for T { xxx } 相当于 T ∈ (A ∩ B)。
-
Rust 编程的哲学是组合优于继承,Rust 并不提供类型层面上的继承。
3. 抽象类型
-
抽象类型属于类型系统的一种,叫作 存在类型(Existential Type)。
-
抽象类型无法直接实例化,它的每个实例都是具体类型的实例。
-
对于抽象类型,编译器可能无法确定其确切的功能和所占空间大小。
-
Rust目前有两种方法来处理抽象类型: trait 对象 和 impl trait。
3.1 trait 对象
-
在 泛型 中使用 trait 限定,可以将任意类型的范围根据类型的行为限定到更精确可控的范围内。
-
从这个角度出发,可以将 共同拥有相同行为的类型集合 抽象为一个类型,这就是 trait Object。
fn main() {
let dog = Dog {
name: "Xiaohei".to_string(),
};
static_dispatch(&dog);
dynamic_dispatch(&dog);
}
#[derive(Debug)]
struct Dog {
name: String,
}
trait Fly {
fn fly(&self);
}
impl Fly for Dog {
fn fly(&self) {
println!("{:?} can not fly", self);
}
}
// 这是带 trait 限定的泛型函数
fn static_dispatch<T>(t: &T)
where
T: Fly,
{
t.fly();
}
// 这是使用 trait 对象的函数
fn dynamic_dispatch(t: &dyn Fly) {
t.fly();
}
-
对于 “使用 trait 对象的函数”,在编译期,编译器只知道 trait Object 包含指针的信息,并且指针的大小也是确定的,但并不知道要具体调用哪个方法。
-
在运行期,当有 trait_object.method() 方法被调用时, TraitObject 会根据虚表指针从虚表中查出正确的函数指针,然后再进行动态调用。 此类函数调用,叫做动态分发。
4. 标签 trait
当 trait 主要是用作某些具有某种行为约束的类型的标签的时候,就叫做 trait 标签。
Rust 在 std::marker 模块中,提供了5个重要的标签 trait :
- Sized trait, 标识 编译器可以确定大小的类型。
- Unsize trait,标识 动态大小类型(DST)。
- Copy trait, 标识 可以安全地按位复制其值的类型。
- Send trait, 标识 可以跨现场安全通信的类型。
- Sync trait, 标识 可以在线程间安全共享引用的类型。
Sized trait
Sized trait,编译器用它来识别可以在编译期确定大小的类型。
-
Sized trait 是一个空 trait,仅仅作为标签 trait 供编译器使用。
-
Rust 语言中大部分类型都是默认 Sized 的,所以在写泛型结构体的时候,没有显式地加上 Sized trait限定。
struct Dog<T>(T); // 等价于 struct Dog<T: Sized>(T); // 如果需要在结构体中使用动态大小类型,则需要改为 <T: ?Sized> 限定。 struct Cat<T: ?Sized>(T); -
?Sized 标识的类型 包含了 Sized 和 Unsize 所标识的两种类型。
-
所以泛型结构体 Cat<T: ?Sized> 支持编译期可确定大小类型和动态大小类型两种类型。
动态大小类型不能随意使用,还需要遵循如下三条限制规则:
- 只可以通过胖指针来操作 Unsized 类型,比如 &[T] 或 &Trait。
- 变量、参数 和 枚举变量 不能使用动态大小类型。
- 结构体中只有最后一个字段可以使用动态大小类型,其他字段不可以使用。
Copy trait
Copy trait 用来标记可以安全地按位复制其值的类型,安位复制相当于 C 语言中的 memcpy。
-
Copy trait 继承自 Clone trait,Clone trait 继承自 Sized,意味着要实现 Copy trait,必须是 Sized 类型,也必须实现 Clone trait。
-
可以使用 #[derive(Copy, Clone)] 来实现 Copy 和 Clone trait。
-
Rust 为很多基本数据类型实现了 Copy trait,比如常用的数字类型、字符 Char、布尔类型、单元值、不可变引用,数组等。
测试类型是否实现了 Copy trait:
fn main() {
let a = "aaa".to_string();
copy_test(a); // Compile error: the trait `Copy` is not implemented for `String`
let a = [1];
copy_test(a);
}
fn copy_test<T: Copy>(t: T) {
println!("test copy");
}
Send trait 和 Sync trait
不同的线程同时访问一块共享变量会造成 数据竞争(Data Race)。
Rust 提供了 Send 和 Sync 两个标签 trait,它们是 Rust无数据竞争并发的基石。
- 实现了 Send 的类型,可以安全地跨线程传递所有权。
- 实现了 Sync 的类型,可以跨线程安全地传递共享(不可变)引用。
有了这两个标签 trait,Rust编译器就会在编译期间,配合所有权机制,对跨线程数据共享问题进行检查。
详细内容,此处跳过。