正式开始

类型体系的实质:对类型进行界说、检查和处理的工具,保证了某个操作处理的数据类型是咱们所希望的 特设多态:包括运算符重载,是指同一种行为有许多不同的完成; 子类型多态:把子类型当成父类型运用,比方 Cat 当成 Animal 运用

trait

trait 是 Rust 中的接口,它界说了类型运用这个接口的行为

基本trait

示例:std::io::Write 接口

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;
    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize> { ... }
    fn is_write_vectored(&self) -> bool { ... }
    fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
    fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... }
    fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... }
    fn by_ref(&mut self) -> &mut Self where Self: Sized { ... }
}
  1. 在 trait 中界说的办法亦被称作相关函数,其能够有缺省的完成
  2. Self 代表当时的类型,比方 File 类型完成了 Write,那么完成过程中运用到的 Self 就指代 File。
  3. self 在用作办法的第一个参数时,实际上是 self: Self 的简写,所以 &self 是 self: &Self, 而 &mut self 是 self: &mut Self
  4. 在完成 trait 的时分,也能够用泛型参数来完成 trait

trait办法是怎样调用的呢

fn write_all(&mut self, buf: &[u8]) -> Result<()>

buf.write_all(b"Hello world!").unwrap(); 为例

  1. 它承受两个参数:&mut self 和 &[u8]
  2. 第一个参数传递的是 buf 这个变量的可变引证
  3. 第二个参数传递的是 b”Hello world!”

带相关类型的 trait

  1. Rust 答应 trait 内部包括相关类型,完成时跟相关函数相同,它也需求完成相关类型
  2. trait 办法里的参数或许回来值,都能够用相关类型来表述,而在完成有相关类型的 trait 时,只需求额外供给相关类型的详细类型即可
pub trait Parse {
    type Error;
    fn parse(s: &str) -> Result<Self, Self::Error>;
}

支撑泛型的 trait


pub trait Add<Rhs = Self> {
    type Output;
    #[must_use]
    fn add(self, rhs: Rhs) -> Self::Output;
}

这儿 Rhs 默认是 Self,也便是说你用 Add trait ,假如不供给泛型参数,那么加号右值和左值都要是相同的类型

  1. 泛型 trait 能够让咱们在需求的时分,对同一种类型的同一个 trait,有多个完成

举例


// Service trait 答应某个 service 的完成能处理多个不同的 Request
pub trait Service<Request> {
    type Response;
    type Error;
    // Future 类型受 Future trait 束缚
    type Future: Future;
    fn poll_ready(
        &mut self, 
        cx: &mut Context<'_>
    ) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}
  1. 留意对于某个确定的 Request 类型,只会回来一种 Response,所以这儿 Response 运用相关类型,而非泛型。
  2. 假如有或许回来多个 Response,那么应该运用泛型 Service
    13|类型系统:如何使用trait来定义接口?

trait 的“承继”

  1. 在 Rust 中,一个 trait 能够“承继”另一个 trait 的相关类型和相关函数
  2. 比方 trait B: A,trait B 在界说时能够运用 trait A 中的相关类型和办法

如何做子类型多态?

假如一个目标 A 是目标 B 的子类,那么 A 的实例能够出现在任何希望 B 的实例的上下文中

fn name(animal: impl Animal) -> &'static str; 等于 fn name<T: Animal>(animal: T) -> &'static str;

  1. 这种泛型函数会根据详细运用的类型被单态化,编译成多个实例,是静态分配

trait object

  1. 咱们要有一种手段告知编译器,此处需求并且仅需求任何完成了 Formatter 接口的数据类型
  2. 在 Rust 里,这种类型叫 Trait Object,表现为 &dyn Trait 或许 Box<dyn Trait> dyn 关键字仅仅用来协助咱们更好地区分普通类型和 Trait 类型,阅览代码时,看到 dyn 就知道后面跟的是一个 trait 了

Trait Object 的完成机理

当需求运用 Formatter trait 做动态分配时,将一个详细类型的引证赋给 &Formatter

13|类型系统:如何使用trait来定义接口?

  1. HtmlFormatter 的引证赋值给 Formatter 后,会生成一个 Trait Object
  2. Trait Object 的底层逻辑便是胖指针。其中,一个指针指向数据自身,另一个则指向虚函数表(vtable)
vtable
  1. vtable 是一张静态的表
  2. Rust 在编译时会为运用了 trait object 的类型的 trait 完成生成一张表,放在可执行文件中(一般在 TEXT 或 RODATA 段)
  3. 一个类型+Trait生成一张表

13|类型系统:如何使用trait来定义接口?
在这张表里,包括详细类型的一些信息,如 size、aligment 以及一系列函数指针:

  1. 这个接口支撑的一切办法
  2. 详细类型的 drop trait,当 Trait object 被开释,它用来开释其运用的一切资源
  3. C++ / Java 指向 vtable 的指针,在编译时放在类结构里,而 Rust 放在 Trait object 中。这也是为什么 Rust 很简单对原生类型做动态分配,而 C++/Java 不可
目标安全

只要满足目标安全的 trait 才干运用 trait object

  1. 假如 trait 一切的办法,回来值是 Self 或许带着泛型参数,那么这个 trait 就不能发生 trait object
  2. 不答应回来 Self,是因为 trait object 在发生时,原来的类型会被抹去,所以 Self 究竟是谁不知道
  3. 不答应带着泛型参数,是因为 Rust 里带泛型的类型在编译时会做单态化,而 trait object 是运行时的产品,两者不能兼容

小结

13|类型系统:如何使用trait来定义接口?

好用链接

  1. io Write 缺省完成
  2. BufBuilder 的Write trait
  3. Rust 正则表达式
  4. 字符串转数字
  5. Add trait
  6. Towner service
  7. gRPC tonic
  8. tokio AsyncWriteExt
  9. futures SteamExt
  10. 目标安全
  11. 迭代器 trait
  12. 完成async fn 的 trait比较困难

延伸阅览

运用 trait 有两个留意事项

  1. 在界说和运用 trait 时,咱们需求遵循孤儿规则(Orphan Rule)

    a. trait 和完成 trait 的数据类型,至少有一个是在当时 crate 中界说的

    b. 也便是说,你不能为第三方的类型完成第三方的 trait,当你测验这么做时,Rust 编译器会报错

  2. Rust 对含有 async fn 的 trait ,还没有一个很好的被标准库承受的完成

vtable 会为每个类型的每个 trait 完成生成一张表

经过以下代码追踪它的行为


use std::fmt::{Debug, Display};
use std::mem::transmute;
fn main() {
    let s1 = String::from("hello world!");
    let s2 = String::from("goodbye world!");
    // Display / Debug trait object for s
    let w1: &dyn Display = &s1;
    let w2: &dyn Debug = &s1;
    // Display / Debug trait object for s1
    let w3: &dyn Display = &s2;
    let w4: &dyn Debug = &s2;
    // 强行把 triat object 转换成两个地址 (usize, usize)
    // 这是不安全的,所以是 unsafe
    let (addr1, vtable1): (usize, usize) = unsafe { transmute(w1) };
    let (addr2, vtable2): (usize, usize) = unsafe { transmute(w2) };
    let (addr3, vtable3): (usize, usize) = unsafe { transmute(w3) };
    let (addr4, vtable4): (usize, usize) = unsafe { transmute(w4) };
    // s 和 s1 在栈上的地址,以及 main 在 TEXT 段的地址
    println!(
        "s1: {:p}, s2: {:p}, main(): {:p}",
        &s1, &s2, main as *const ()
    );
    // trait object(s / Display) 的 ptr 地址和 vtable 地址
    println!("addr1: 0x{:x}, vtable1: 0x{:x}", addr1, vtable1);
    // trait object(s / Debug) 的 ptr 地址和 vtable 地址
    println!("addr2: 0x{:x}, vtable2: 0x{:x}", addr2, vtable2);
    // trait object(s1 / Display) 的 ptr 地址和 vtable 地址
    println!("addr3: 0x{:x}, vtable3: 0x{:x}", addr3, vtable3);
    // trait object(s1 / Display) 的 ptr 地址和 vtable 地址
    println!("addr4: 0x{:x}, vtable4: 0x{:x}", addr4, vtable4);
    // 指向同一个数据的 trait object 其 ptr 地址相同
    assert_eq!(addr1, addr2);
    assert_eq!(addr3, addr4);
    // 指向同一种类型的同一个 trait 的 vtable 地址相同
    // 这儿都是 String + Display
    assert_eq!(vtable1, vtable3);
    // 这儿都是 String + Debug
    assert_eq!(vtable2, vtable4);
}

精选问答

  1. 对于 Addtrait,假如咱们不用泛型,把 Rhs 作为 Add trait 的相关类型,能够么?为什么?

    不能够。相关类型只能impl一次,咱们需求为Complex完成多个Add<Rhs>

    a. trait 泛型是对同一个数据结构需求有多个不同的完成

    b. trait 的相关类型是,在某个完成里,我需求设定和这个完成相关的类型。其实相关类型就和相关函数相同的

  2. 如下代码能编译经过么,为什么?


use std::{fs::File, io::Write};
fn main() {
    let mut f = File::create("/tmp/test_write_trait").unwrap();
    let w: &mut dyn Write = &mut f;
    w.write_all(b"hello ").unwrap();
    let w1 = w.by_ref();
    w1.write_all(b"world").unwrap();
}

不能。回来类型中的 Self 需求是Sized,而 dyn Write 不是Sized