笔记-Rust类型系统3: trait

563 阅读7分钟

深入 trait

Rust 中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象,均基于 trait 来完成。

trait 有以下4种用法:

  1. 接口抽象: 接口是对类型行为的统一约束。
  2. 泛型约束: 泛型的行为被 trait 限定在更有限的范围内。
  3. 抽象类型: 在运行时作为一种间接的抽象类型去使用,动态地分发给具体的类型。
  4. 标签 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编译器就会在编译期间,配合所有权机制,对跨线程数据共享问题进行检查。

详细内容,此处跳过。