用类型表达:没有 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 都只能是一个类型。

改成 Vec<Box<dyn Greet>> 就通了:

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 就不行:

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/) —— 这也是它在真实代码里最常见的用途之一。