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 是有点多余的:

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

  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);
}

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