trait 是 Rust 的灵魂,Rust 中的所有抽象,如接口抽象,OOP 范式抽象,函数式范式抽象,都是基于 trait 完成的。
trait 是在行为上对类型的约束,这种约束让 trait 有如下四种用法:
- 接口抽象。接口是对类型行为的同一约束。
- 泛型约束。泛型的行为被 trait 限定在更有限的范围内。
- 抽象类型。在运行时作为一种间接的抽象去使用,动态的分发给具体的类型。
- 标签 triat 。对类型的约束,可以直接作为一种标签使用。
接口抽象
trait 的接口抽象有如下特点:
- 接口中可以定义方法,并支持默认实现。
- 接口中不能实现另一个接口,但是接口之间可以继承。
- 同一个接口可以同时被多个类型实现,但不能被同一个类型实现多次。
- 使用 impl 关键字为类型实现接口方法。
- 使用 trait 关键字定义接口。
关联类型
Rust 中的很多操作符都是基于 trait 来实现的。比如加法操作符就是一个 trait 。
下面是利用 trait 实现加法操作的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // add trait ,定义 my_add 接口
trait Add<RHS, OUTPUT> {
fn my_add(self, rhs: RHS) -> OUTPUT;
}
// 为 i32 实现加法操作
impl Add<i32, i32> for i32 {
fn my_add(self, rhs: i32) -> i32 {
self + rhs
}
}
// 为 u32 实现加法操作
impl Add<u32, i32> for u32 {
fn my_add(self, rhs: u32) -> i32 {
(self + rhs) as i32
}
}
fn main() {
let (a, b, c, d) = (1i32, 2i32, 3u32, 4u32);
let x = a.my_add(b);
let y = c.my_add(d);
println!("x: {}, y: {}", x, y);
}
|
在上面的实现的 Add 中,OUTPUT 是有点多余的:
- 对于基础类型,i32 和 i32 相加,返回也必须是 i32 才能保证安全。
- 对于字符串相加,String 加上 str 返回也一定是 String
标准库实现的 Add 如下:
1
2
3
4
5
6
7
8
9
10
11
| // 表示 RHS 的默认类型是 Self ,Self 是每个 trait 的隐式类型参数,表示实现当前 trait 的具体类型。
pub trait Add<RHS = Self> {
type Output; // 这种方式定义的类型叫做关联类型
fn add(self, rhs: RHS) -> Self::Output;
}
// 为 u32 实现的 add
impl Add for u32 {
type Output = u32;
fn add(self, other: u32) -> u32 { self + other }
}
|
使用关联类型,对代码进行了精简,同时也对方法的输入输出进行了隔离。
trait 一致性
u32 类型和 u64 类型不能直接相加,如果我们试图重载这个行为,编译器会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
| use std::ops::Add;
impl Add<u64> for u32 {
type Output = u64;
fn add(self, other: u64) -> Self::Output {
(self as u64) + other
}
}
fn main() {
let a = 1u32;
let b= 2u64;
let x = a + b;
}
|
1
2
3
4
5
6
7
8
9
10
| error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src\main.rs:3:1
|
3 | impl Add<u64> for u32 {
| ^^^^^--------^^^^^---
| | | |
| | | `u32` is not defined in the current crate
| | `u64` is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
|
这是因为 Rust 有一条最重要的规则:孤儿规则 。
孤儿规则规定:如果要实现某个 trait ,那么该 trait 和 实现该 trait 的类型必须要有一个在 当前 crate 中定义。
在上面的代码中,Add 和 u32, u64 都不是在当前 crate 中定义的,而是定义在标准库中。这个规则是为了防止对标准库中
u32 的加法行为进行破坏性的改写。
trait 继承
Rust 支持 trait 继承。子 trait 可以继承父 trait 中定义的或实现的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| trait Page {
fn set_page(&self, _p: i32) {
println!("Page Default: 1");
}
}
trait PerPage {
fn set_perpage(&self, _num: i32) {
println!("Per Page Default: 10");
}
}
// 继承其它 trait
trait Paginate: Page + PerPage {
fn set_skip_page(&self, num: i32) {
println!("Skip Page: {:?}", num);
}
}
struct MyPaginate {
page: i32,
}
impl Page for MyPaginate {}
impl PerPage for MyPaginate {}
// 实现了 Page 和 PerPage trait 的实现 Paginate
// 这里是泛型约束
impl<T: Page + PerPage> Paginate for T {}
fn main() {
let my_paginate = MyPaginate { page: 1 };
let _p = my_paginate.page;
my_paginate.set_page(2);
my_paginate.set_perpage(100);
my_paginate.set_skip_page(12);
}
|
泛型约束
在使用泛型编程时,很多情况下的行为并不是针对所有类型实现的,如下面的求和例子:
1
2
3
| fn sum<T>(a: T, b: T) -> T {
a + b
}
|
对于 sum 来说,必须要两个参数是相加的才可以,如修正后的代码:
1
2
3
| fn sum<T: Add<T, Output=T>>(a: T, b: T) -> T {
a + b
}
|
使用 trait 对泛型进行限定,叫做 trait 限定 。
impl<T: A + B> C for T
的意思就是:
为所有的 T ,T 必须满足条件:T 同时实现了 trait A 的所有方法以及 trait B 的所有方法,
实现 trait C 。
为泛型增加比较多的 trait 限定时,代码可能会变得不太易读,比如:
1
| fn foo<T: A, K: B+C, R:D>(a: T, b: K, c: R) {}
|
Rust 提供 where 关键字,用来对付这种情况。
1
2
3
4
| fn foo<T, K, R>(a: T, b: K, c: R)
where T: A, K: B+C, R: D {
}
|
抽象类型
trait 可以作为抽象类型。相对于具体类型而言,抽象类型无法实例化。
对于抽象类型而言,编译器可能无法确定其确切的功能和所占空间的大小。
所以 Rust 目前有两种方法处理抽象类型:
trait 对象和 impl Trait 。
trait 对象
将共同拥有相同行为的类型集合抽象为一个类型,这就是 trait 对象。
trait 限定和 trait 对象的用法比较:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #[derive(Debug)]
struct Foo;
trait Bar {
fn baz(&self);
}
impl Bar for Foo {
fn baz(&self) {
println!("{:?}", self);
}
}
fn static_dispatch<T>(t: &T) where T: Bar {
t.baz();
}
fn dynamic_dispatch<T>(t: &dyn Bar) {
t.baz();
}
fn main() {
let foo = Foo;
static_dispatch(&foo);
dynamic_dispatch(&foo);
}
|
trait 类型在编译期无法确定大小,所以 trait 对象必须使用胖指针表示。
1
2
3
4
| pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut ()
}
|
TraitObject 包括两个指针,data 指针 和 vtable 指针。
以 impl MyTrait for T
为例,data 指针指向 trait 对象保存的类型数据 T 。
vtable 指针指向包含为 T 实现的 MyTrait 的 VTable 。虚表的本质是一个结构体,
包括了析构函数,大小,对齐。
在编译期,编译器只知道 TraitObject 包含指针信息,并且指针的大小也是确定的。
在运行期,当 trait_object.method()
方法被调用时,Trait 对象会根据虚表指针从虚表
中查出正确的指针,然后在进行动态调用,所以将 trait 对象称为动态分发。
trait 对象安全
TODO: 还需要加强理解
每个 trait 都包含一个隐式的类型参数 Self ,代表实现该 trait 的类型。Self 默认有一个隐式的 trait 限定 ?Sized ,
形如 <Self: ?Sized>
,?Sized trait 包括了所有的动态大小类型和所有可确定大小的类型。Rust 中大部分类型默认是
可确定大小的类型,也就是 <T:Sized>
。
当 trait 对象在运行期进行动态分发时,在运行时已经擦除了具体的类型信息,为了保证可以正常的调用方法,必须同时满足
以下两条规则的 trait 才可以作为 trait 对象使用:
- trait 的 Self 类型参数不能被限定为 Sized 。
- trait 中所有的方法都必须是对象安全的。
1
2
3
| trait Foo : Sized {
fn some_method(&self);
}
|
Foo 继承自 Sized ,这表明要为某类型实现 Foo ,必须先实现 Sized 。所以,Foo 中隐式的 Self 也必然是
Sized 的。因为 Self 代表的是那些要实现 Foo 的类型。
trait 对象本身是动态分发的,编译期根本无法确定 Self 具体是哪个类型,因为不知道给哪些类型实现过该 trait ,
更无法确定其大小,现在又要求 Self 是可确定大小的,就会造成冲突。
当把 trait 当作对象使用的时候,其内部类型就默认为 Unsize 类型。
对象安全的方法必须满足以下条件之一:
- 方法受到 Self:Sized 限定
- 方法签名必须满足以下三点:
- 必须不包含任何泛型参数,如果包括泛型,trait 对象在虚表中查找方法时不知道调用哪个方法。
- 第一个参数必须为 Self 类型或可以解引用为 Self 的类型,没有接收者的方法对 trait 对象毫无意义。
- Self 对象不能出现在除第一个参数之外的任何地方
1
2
3
4
5
| trait Bar {
fn bax(self,x: u32);
fn bay(&self);
fn baz(&mut self);
}
|
对象不安全的 trait :
1
2
3
4
| trait Foo {
fn foo(&self) -> int { ... }
fn new() -> Self;// 返回 Self
}
|
将对象安全和对象不安全的方法分开。
1
2
3
4
5
6
7
8
9
| // Foo 现在是对象安全的了
trait Foo {
fn foo(&self) -> int {...}
}
// Bar 对象不安全
trait Bar: Foo {
fn new() -> Self;
}
|
可以使用 Sized 限定:
1
2
3
4
| trait Foo {
fn foo(&self) -> int {...}
fn new() -> Self where Self: Sized;
}
|
受到 Sized 限定方法不会参与动态分发。
impl Trait
impl Trait 是可以静态分发的抽象类型。目前 impl Trait 只可以在输入的参数和返回值两个地方使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| use std::fmt::Debug;
pub trait Fly {
fn fly(&self) -> bool;
}
#[derive(Debug)]
struct Duck;
#[derive(Debug)]
struct Pig;
impl Fly for Duck {
fn fly(&self) -> bool {
return true;
}
}
impl Fly for Pig {
fn fly(&self) -> bool {
return false;
}
}
fn fly_static(s: impl Fly + Debug) -> bool {
s.fly()
}
fn can_fly(s: impl Fly + Debug) -> impl Fly {
if s.fly() {
println!("{:?} can fly!", s);
} else {
println!("{:?} cann't fly", s);
}
s
}
fn main() {
let pig = Pig;
println!("{:?}", fly_static(pig));
let pig = Pig;
can_fly(pig);
}
|
在 Rust 2018 后,为了于 impl Trait 对应,专门为动态分发增加了 dyn Trait 。
即 impl 表示静态分发,dyn 表示动态分发。
1
2
3
4
5
6
7
8
9
| // 形如 Box<dyn Fly> 就是返回 trait 对象
fn dyn_can_fly(s: dyn Fly + Debug + 'static) -> Box<dyn Fly> {
if s.fly() {
println!("{:?} can fly!", s);
} else {
println!("{:?} cann't fly",s);
}
Box::new(s)
}
|
标签 trait
trait 这种对行为约束的特性也非常适合作为类型的标签。这就像市场上流通的产品,都被厂商盖上了
“生产日期”和“有效期”这样的标签,消费者通过这种标签就可以识别出未过期的产品。
Rust 就是厂家,类型就是产品,标签 trait 就是 “厂家”给“产品”盖上的各种标签,起到标识的作用。
当开发者消费这些类型的“产品”时。编译器会进行“严格执法”,以保证这些类型“产品”是合格的。
Rust 在标准库 std::marker
模块中定义了五个重要的标签:
Sized
trait ,用来标识编译期可确定大小的类型。Unsize
trait ,目前该 trait 为实验特性,用于标识动态大小类型(DST)。Copy
trait ,用来标识可以安全地按位复制其值的类型。Send
trait ,用来标识可以跨线程安全通信的类型。Sync
trait ,用来标识可以在线程间安全共享引用的类型。
Sized trait
1
2
| #[lang = "sized"]
pub trait Sized {}
|
Sized trait 是一个空 trait ,仅仅作为标签 trait 供编译器使用。这里真正起“打标签”作用的是
#[lang = "sized"]
,该属性 lang 表示 Sized trait 供 Rust 语言本身使用,声明为 “sized” 称为语言项。
Rust 中大部分类型都是默认 Sized 的,所以在写泛型结构体时,没有显示的加 Sized 限定。
1
2
| struct Foo<T>(T);
struct Bar<T:?Sized>(T);
|
如果要在结构体中使用动态类型大小,则要改为 <T:?Sized> 限定。?Sized 是 Sized 的另一种写法,
包括 Sized 和 Unsize 两个范围。
Unsize 标记动态类型,目前 Rust 中有的动态类型是 trait 和 [T] ,其中 [T] 表示一定数量的 T 在
内存中依次排列,但是不知道具体的数量。
动态类型大小有三条限制:
- 只可以通过胖指针来操作 Unsize 类型,比如 &[T] 和 &trait ;
- 变量,参数和枚举变量不能使用动态大小类型;
- 结构体中只有最后一个字段可以使用动态大小类型;
Copy trait
标识可以安全按位复制的类型,按位复制等价于 C 语言中的 memcpy 。
1
2
| #[lang = "copy"]
pub trait Copy : Clone {}
|
Copy trait 继承 Clone trait ,要实现 Copy trait ,必须实现 Clone trait 定义的方法。
1
2
3
4
5
6
| pub trait Clone : Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone();
}
}
|
Clone trait 继承自 Sized ,意味着要实现 Clone trait 的对象必须是 Sized 类型。
Send trait 和 Sync trait
这两个 trait 是 Rust 无数据竞争并发的基石。
- 实现了 Send 的类型,可以安全的在线程间传递值,也就是可以跨线程传递所有权。
- 实现了 Sync 的类型,可以跨线程安全的传递共享(不可变)引用。
有了这两个 trait ,Rust 能在编译期就检查出数据竞争的隐患,而不需要等到运行时再排查。