用类型表达:没有 null,也没有异常
泛型、trait 与 dyn
它在解决什么
这一章一路在用泛型 —— Option<T>、Result<T, E>、Vec<T> ——
却一直没解释它。这一篇补上,顺带回答那个从 Java/C# 过来的人一定会问的问题:
Rust 的 trait 是接口吗?
不完全是。接口通常回答「这个对象是什么」,而 trait 回答 「这个类型能做什么」。差别看起来很虚,但它带来一个很实在的后果: 你可以给一个早就写好的类型补上新能力,包括别人写的类型。
泛型 + trait bound:一份代码,编译期长成多份
fn greet<T: Greet>(x: &T) -> String {
x.hello()
}
<T: Greet> 读作「任何实现了 Greet 的类型」。
给它传 Cat 和 Dog 都行,
传一个没实现 Greet 的类型就是 E0277。
关键在于这份代码在编译期会被单态化(monomorphization):
编译器为每个真正用到的 T 各生成一份机器码。所以
- 运行时没有任何查表开销,和你手写两个函数一样快;
- 代价是编译产物变大、编译变慢 —— 泛型用得越狠,这两样越明显。
📌 这就是「零成本抽象」在这里的具体含义:抽象发生在编译期,运行时不留痕迹。
🚨 什么时候不得不用 dyn
单态化有一个硬边界:它要求编译期就知道具体类型。 于是这段代码编译不过:
let zoo = vec![Cat, Dog]; // E0308
Vec<T> 里每个元素必须是同一个 T,
而 Cat 和 Dog 是两个类型。泛型在这里救不了你 ——
无论怎么写 <T: Greet>,那个 T 都只能是一个类型。
let zoo: Vec<Box<dyn Greet>> = vec![Box::new(Cat), Box::new(Dog)];
dyn Greet 的意思是「某个实现了 Greet 的东西,具体是谁运行时才知道」。
它靠一张虚表(vtable)在运行时找到该调哪个 hello。
⇒ 判据很清楚:能用泛型就用泛型;需要在一个集合里混装不同类型时,才换 dyn。
代价是两样:多一次指针跳转(查虚表),以及 Box 带来的一次堆分配。
在绝大多数应用里这点开销无所谓 —— 但它确实不是零。
泛型 <T: Trait> |
dyn Trait |
|
|---|---|---|
| 决定调哪个实现 | 编译期 | 运行时(虚表) |
| 运行时开销 | 无 | 一次指针跳转 |
| 编译产物 | 每个 T 一份 | 一份 |
| 能混装不同类型 | 不能 | 能 |
dyn 的代价之二:不是所有 trait 都能做成它
trait Spawn {
fn make() -> Self;
}
make 没有 self 参数,而且返回 Self。通过一个 dyn Spawn 调它时,
编译器不知道该造出哪种东西,也没有对象可以查虚表 —— 报 E0038。
这类 trait 叫「不是对象安全的」(object safe)。完整规则挺细, 但有个够用的经验:
方法带
&self、不返回Self、方法本身不带泛型参数,基本就能做dyn。
不用 dyn、直接写具体类型就没事 ——
对象安全是 dyn 的要求,不是 trait 本身的毛病。
默认方法:用必需的拼出便利的
trait 里的方法可以带默认实现:
trait Greet {
fn name(&self) -> String; // 必需
fn hello(&self) -> String { // 有默认实现
format!("你好,{}", self.name())
}
}
Cat 不写 hello 也能用,Dog 写了就覆盖掉。
但 name 没有默认实现,不写就是 E0046,
而且编译器会点名还差哪几个。
⭐ 这个机制在标准库里用到了极致:Iterator 只要求你实现 next,
map / filter / take / zip 那七十多个方法全是拿 next 拼出来的默认方法。
derive:让编译器替你写 impl
{:?} 要 Debug、.clone() 要 Clone、== 要 PartialEq ——
这三样都不是语言内置的,全是 trait。
#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }
derive 只是按字段逐个生成一份最直白的实现。
整行删掉,那三个操作就全没了。
⇒ 所以「为什么我的结构体不能 println!("{:?}")」的答案永远是同一个:
你没给它那个能力。
孤儿规则,以及 newtype 的又一次登场
trait 最诱人的能力是「给别人的类型补能力」。但它有一条边界:
trait 和类型,至少有一个得是你自己的。
给 Vec<i32> 实现 Display 就是 E0117 ——
两边都是标准库的。
这条规则叫孤儿规则,理由很实际:如果谁都能给别人的类型实现别人的 trait, 两个 crate 各写一份,编译器就不知道该用哪个了。
绕开的办法是包一层 newtype:
struct MyVec(Vec<i32>);
impl fmt::Display for MyVec { /* ... */ }
MyVec 是你的类型,于是随便实现。这也是
newtype 在真实代码里最常见的用途之一 ——
它不只是防呆,还是绕开孤儿规则的标准手法。
这一章到这里就完整了
| 篇 | 它回答的问题 |
|---|---|
| struct 与 enum | 怎么把「有哪几种情况」写进类型 |
| 模式匹配 | 怎么保证每种情况都被处理到 |
| Option | 「可能没有」怎么表达 |
| Result 与 ? | 「可能失败,以及为什么」怎么表达 |
| 本篇 | 怎么描述「一类类型共有的能力」 |
下一步
前两章讲的是怎么写对。接下来换一个层面:怎么把代码组织成一个能维护的项目 —— 从《模块、crate 与 workspace》开始。
全部篇目见 Rust 教程首页。
本篇示例
下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。
泛型 + trait bound:一份代码,编译期长成多份
trait Greet {
fn hello(&self) -> String;
}
struct Cat;
struct Dog;
impl Greet for Cat {
fn hello(&self) -> String { String::from("喵") }
}
impl Greet for Dog {
fn hello(&self) -> String { String::from("汪") }
}
fn greet<T: Greet>(x: &T) -> String {
x.hello()
}
fn main() {
println!("{} {}", greet(&Cat), greet(&Dog));
}编译通过 · 输出 "喵 汪\n"
对照:传一个没实现 `Greet` 的类型进去
trait Greet {
fn hello(&self) -> String;
}
struct Cat;
struct Fish;
impl Greet for Cat {
fn hello(&self) -> String { String::from("喵") }
}
fn greet<T: Greet>(x: &T) -> String {
x.hello()
}
fn main() {
println!("{} {}", greet(&Cat), greet(&Fish));
}编译不过:error[E0277]
`<T: Greet>` 读作「任何实现了 `Greet` 的类型」。编译器会为每个真正用到的 `T` **各生成一份机器码**(单态化),所以运行时没有任何查表开销 —— 和你手写两个函数一样快。对照项报 `E0277`:`Fish` 没有那个能力。
🚨 唯一非用 `dyn` 不可的场合:一个容器装多种类型
trait Greet {
fn hello(&self) -> String;
}
struct Cat;
struct Dog;
impl Greet for Cat {
fn hello(&self) -> String { String::from("喵") }
}
impl Greet for Dog {
fn hello(&self) -> String { String::from("汪") }
}
fn main() {
let zoo = vec![Cat, Dog];
for a in &zoo {
println!("{}", a.hello());
}
}编译不过 · error[E0308]
rustc 原文
error[E0308]: mismatched types
--> trait-dyn-for-mixed-container.rs:17:25
|
17 | let zoo = vec![Cat, Dog];
| ^^^ expected `Cat`, found `Dog`对照:改成 `Vec<Box<dyn Greet>>`
trait Greet {
fn hello(&self) -> String;
}
struct Cat;
struct Dog;
impl Greet for Cat {
fn hello(&self) -> String { String::from("喵") }
}
impl Greet for Dog {
fn hello(&self) -> String { String::from("汪") }
}
fn main() {
let zoo: Vec<Box<dyn Greet>> = vec![Box::new(Cat), Box::new(Dog)];
for a in &zoo {
println!("{}", a.hello());
}
}编译通过,输出:"喵\n汪\n"
⭐ 这条是整篇的支点。`Vec<T>` 里每个元素必须是**同一个** `T`,而 `Cat` 和 `Dog` 是两个类型 —— 泛型救不了,因为单态化要求编译期就知道具体类型。`dyn Greet` 把「具体是谁」推迟到运行时(靠一张虚表),代价是多一次指针跳转和一次堆分配(`Box`)。⇒ 判据:**能用泛型就用泛型;需要在一个集合里混装不同类型时,才换 `dyn`。**
`dyn` 的代价:不是所有 trait 都能做成 trait 对象
trait Spawn {
fn make() -> Self;
}
struct Cat;
impl Spawn for Cat {
fn make() -> Self { Cat }
}
fn main() {
let _x: Box<dyn Spawn> = Box::new(Cat::make());
println!("ok");
}编译不过 · error[E0038]
rustc 原文
error[E0038]: the trait `Spawn` is not dyn compatible
--> trait-object-safety.rs:12:21
|
12 | let _x: Box<dyn Spawn> = Box::new(Cat::make());
| ^^^^^ `Spawn` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> trait-object-safety.rs:2:8
|
1 | trait Spawn {
| ----- this trait is not dyn compatible...
2 | fn make() -> Self;
| ^^^^ ...because associated function `make` has no `self` parameter
= help: only type `Cat` implements `Spawn`; consider using it directly instead.
help: consider turning `make` into a method by giving it a `&self` argument
|
2 | fn make(&self) -> Self;
| +++++
help: alternatively, consider constraining `make` so it does not apply to trait objects
|
2 | fn make() -> Self where Self: Sized;
| +++++++++++++++++对照:不用 `dyn`,直接写具体类型
trait Spawn {
fn make() -> Self;
}
struct Cat;
impl Spawn for Cat {
fn make() -> Self { Cat }
}
fn main() {
let _x: Cat = Cat::make();
println!("ok");
}编译通过,输出:"ok\n"
`fn make() -> Self` 没有 `self` 参数,而且返回 `Self` —— 通过一个 `dyn Spawn` 调它时,编译器不知道该造出哪种东西,也没有对象可以查虚表。这类 trait 叫**不是对象安全的**(`E0038`)。⇒ 一个够用的经验:**方法带 `&self`、不返回 `Self`、不带泛型参数**,基本就能做 `dyn`。
trait 可以带默认实现
trait Greet {
fn name(&self) -> String;
fn hello(&self) -> String {
format!("你好,{}", self.name())
}
}
struct Cat;
struct Dog;
impl Greet for Cat {
fn name(&self) -> String { String::from("猫") }
}
impl Greet for Dog {
fn name(&self) -> String { String::from("狗") }
fn hello(&self) -> String { String::from("汪!") }
}
fn main() {
println!("{} / {}", Cat.hello(), Dog.hello());
}编译通过 · 输出 "你好,猫 / 汪!\n"
对照:删掉 `Cat` 的 `name` 实现
trait Greet {
fn name(&self) -> String;
fn hello(&self) -> String {
format!("你好,{}", self.name())
}
}
struct Cat;
struct Dog;
impl Greet for Cat {}
impl Greet for Dog {
fn name(&self) -> String { String::from("狗") }
fn hello(&self) -> String { String::from("汪!") }
}
fn main() {
println!("{} / {}", Cat.hello(), Dog.hello());
}编译不过:error[E0046]
`hello` 有默认实现,所以 `Cat` 不写它也能用;`Dog` 写了就覆盖掉。但 `name` **没有**默认实现,不写就是对照项那个 `E0046`(还有哪些没实现,编译器会点名)。⇒ 默认方法是「用已有的必需方法拼出便利方法」,标准库里 `Iterator` 只要求你实现 `next`,其余七十多个方法全是这么来的。
`derive` 就是让编译器替你写 impl
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{:?} {} {}", a, a == b, a == Point { x: 9, y: 9 });
}编译通过 · 输出 "Point { x: 1, y: 2 } true false\n"
对照:把 `#[derive(...)]` 整行删掉
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{:?} {} {}", a, a == b, a == Point { x: 9, y: 9 });
}编译不过:error[E0599]、error[E0369]
`{:?}` 要 `Debug`、`.clone()` 要 `Clone`、`==` 要 `PartialEq` —— 这三样都不是语言内置的,全是 trait。`derive` 只是按字段逐个生成一份最直白的实现。⇒ 所以「为什么我的结构体不能 `println!("{:?}")`」的答案永远是同一个:**你没给它那个能力**。
孤儿规则:别人的类型 + 别人的 trait = 不行
use std::fmt;
impl fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "vec({})", self.len())
}
}
fn main() {
println!("{}", vec![1, 2, 3]);
}编译不过 · error[E0117]
rustc 原文
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> trait-orphan-rule.rs:3:1
|
3 | impl fmt::Display for Vec<i32> {
| ^^^^^^^^^^^^^^^^^^^^^^--------
| |
| `Vec` is not defined in the current crate
|
= note: impl doesn't have any local type before any uncovered type parameters
= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
= note: define and implement a trait or new type instead对照:用 newtype 包一层,变成「我的类型」
use std::fmt;
struct MyVec(Vec<i32>);
impl fmt::Display for MyVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "vec({})", self.0.len())
}
}
fn main() {
println!("{}", MyVec(vec![1, 2, 3]));
}编译通过,输出:"vec(3)\n"
`Display` 是标准库的,`Vec` 也是标准库的,两边都不是你的 ⇒ `E0117`。规则存在的理由很实际:**如果谁都能给别人的类型实现别人的 trait,两个 crate 各写一份,编译器就不知道该用哪个了。** 绕开的办法就是[前面那个 newtype](/rust/types/struct-enum/) —— 这也是它在真实代码里最常见的用途之一。