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);
}
1
x: 3, y: 7

在上面的实现的 Add 中,OUTPUT 是有点多余的:

  1. 对于基础类型,i32 和 i32 相加,返回也必须是 i32 才能保证安全。
  2. 对于字符串相加,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
Page Default: 1
Per Page Default: 10
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);
}
1
error: Could not compile `cargolzpONH`.

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 对象使用:

  1. trait 的 Self 类型参数不能被限定为 Sized 。
  2. 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);
}
1
2
false
Pig cann't fly

在 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 模块中定义了五个重要的标签:

  1. Sized trait ,用来标识编译期可确定大小的类型。
  2. Unsize trait ,目前该 trait 为实验特性,用于标识动态大小类型(DST)。
  3. Copy trait ,用来标识可以安全地按位复制其值的类型。
  4. Send trait ,用来标识可以跨线程安全通信的类型。
  5. 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 在内存中依次排列,但是不知道具体的数量。

动态类型大小有三条限制:

  1. 只可以通过胖指针来操作 Unsize 类型,比如 &[T] 和 &trait ;
  2. 变量,参数和枚举变量不能使用动态大小类型;
  3. 结构体中只有最后一个字段可以使用动态大小类型;

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 能在编译期就检查出数据竞争的隐患,而不需要等到运行时再排查。