正式开始
类型系统的本质:对类型进行定义、检查和处理的工具,保证了某个操作处理的数据类型是我们所希望的 特设多态:包括运算符重载,是指同一种行为有很多不同的实现; 子类型多态:把子类型当成父类型使用,比如 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 { ... }
}
- 在 trait 中定义的方法亦被称作关联函数,其可以有缺省的实现
- Self 代表当前的类型,比如 File 类型实现了 Write,那么实现过程中使用到的 Self 就指代 File。
- self 在用作方法的第一个参数时,实际上是 self: Self 的简写,所以 &self 是 self: &Self, 而 &mut self 是 self: &mut Self
- 在实现 trait 的时候,也可以用泛型参数来实现 trait
trait方法是怎样调用的呢
以 fn write_all(&mut self, buf: &[u8]) -> Result<()>;
buf.write_all(b"Hello world!").unwrap(); 为例
- 它接受两个参数:&mut self 和 &[u8]
- 第一个参数传递的是 buf 这个变量的可变引用
- 第二个参数传递的是 b"Hello world!"
带关联类型的 trait
- Rust 允许 trait 内部包含关联类型,实现时跟关联函数一样,它也需要实现关联类型
- 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 ,如果不提供泛型参数,那么加号右值和左值都要是相同的类型
- 泛型 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;
}
- 注意对于某个确定的 Request 类型,只会返回一种 Response,所以这里 Response 使用关联类型,而非泛型。
- 如果有可能返回多个 Response,那么应该使用泛型 Service
trait 的“继承”
- 在 Rust 中,一个 trait 可以“继承”另一个 trait 的关联类型和关联函数
- 比如 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;
- 这种泛型函数会根据具体使用的类型被单态化,编译成多个实例,是静态分派。
trait object
- 我们要有一种手段告诉编译器,此处需要并且仅需要任何实现了 Formatter 接口的数据类型。
- 在 Rust 里,这种类型叫 Trait Object,表现为 &dyn Trait 或者 Box<dyn Trait> dyn 关键字只是用来帮助我们更好地区分普通类型和 Trait 类型,阅读代码时,看到 dyn 就知道后面跟的是一个 trait 了
Trait Object 的实现机理
当需要使用 Formatter trait 做动态分派时,将一个具体类型的引用赋给 &Formatter
- HtmlFormatter 的引用赋值给 Formatter 后,会生成一个 Trait Object
- Trait Object 的底层逻辑就是胖指针。其中,一个指针指向数据本身,另一个则指向虚函数表(vtable)
vtable
- vtable 是一张静态的表
- Rust 在编译时会为使用了 trait object 的类型的 trait 实现生成一张表,放在可执行文件中(一般在 TEXT 或 RODATA 段)
- 一个类型+Trait生成一张表
在这张表里,包含具体类型的一些信息,如 size、aligment 以及一系列函数指针:
- 这个接口支持的所有方法
- 具体类型的 drop trait,当 Trait object 被释放,它用来释放其使用的所有资源
- C++ / Java 指向 vtable 的指针,在编译时放在类结构里,而 Rust 放在 Trait object 中。这也是为什么 Rust 很容易对原生类型做动态分派,而 C++/Java 不行
对象安全的
只有满足对象安全的 trait 才能使用 trait object
- 如果 trait 所有的方法,返回值是 Self 或者携带泛型参数,那么这个 trait 就不能产生 trait object
- 不允许返回 Self,是因为 trait object 在产生时,原来的类型会被抹去,所以 Self 究竟是谁不知道
- 不允许携带泛型参数,是因为 Rust 里带泛型的类型在编译时会做单态化,而 trait object 是运行时的产物,两者不能兼容
小结
好用链接
- io Write 缺省实现
- BufBuilder 的Write trait
- Rust 正则表达式
- 字符串转数字
- Add trait
- Towner service
- gRPC tonic
- tokio AsyncWriteExt
- futures SteamExt
- 对象安全
- 迭代器 trait
- 实现async fn 的 trait比较困难
延伸阅读
使用 trait 有两个注意事项
-
在定义和使用 trait 时,我们需要遵循孤儿规则(Orphan Rule)
a. trait 和实现 trait 的数据类型,至少有一个是在当前 crate 中定义的
b. 也就是说,你不能为第三方的类型实现第三方的 trait,当你尝试这么做时,Rust 编译器会报错
-
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);
}
精选问答
-
对于 Add trait,如果我们不用泛型,把 Rhs 作为 Add trait 的关联类型,可以么?为什么?
不可以。关联类型只能impl一次,我们需要为Complex实现多个Add<Rhs>
a. trait 泛型是对同一个数据结构需要有多个不同的实现
b. trait 的关联类型是,在某个实现里,我需要设定和这个实现相关的类型。其实关联类型就和关联函数一样的
-
如下代码能编译通过么,为什么?
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