跳转到内容

Rust 高级 trait

在 trait 那一页,你学会了怎样定义并实现一个 trait。在泛型和常用 trait 两页,你用过 Display、From、Add 这些 trait。日常写代码,这些已经够用了。不过,当你去读标准库或者热门 crate 的源码时,会碰到一些乍一看有点怪的 trait 写法:type Item、Add<Rhs = Self>、trait Pretty: Display、impl<T: Display> ... for T。

这一页会把它们一个一个讲清楚。它们都不是什么魔法:每一个都在解决一个具体的小问题,弄懂了问题,语法自然就顺了。慢慢来,读完一节、动手试试代码,过段时间再回来看下一节,完全没问题。

关联类型:每个类型只有一个答案

Section titled “关联类型:每个类型只有一个答案”

其实你已经见过关联类型了:Iterator 里就有 type Item。关联类型(associated type)是 trait 里的一个占位类型,每个实现都要把它确定下来,而且只确定一次。

下面是一个小小的 Container trait。播放列表装的是 String,分数列表装的是 u32,所以它们各自选定自己的 Item:

use std::fmt::Debug;
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> {
self.get(0)
}
}
struct Playlist {
songs: Vec<String>,
}
struct Scores {
values: Vec<u32>,
}
impl Container for Playlist {
type Item = String;
fn get(&self, index: usize) -> Option<&String> {
self.songs.get(index)
}
fn size(&self) -> usize {
self.songs.len()
}
}
impl Container for Scores {
type Item = u32;
fn get(&self, index: usize) -> Option<&u32> {
self.values.get(index)
}
fn size(&self) -> usize {
self.values.len()
}
}
// 只要元素能用 {:?} 打印,任何 Container 都可以传进来
fn describe<C: Container>(c: &C)
where
C::Item: Debug,
{
println!("共 {} 个元素,第一个是 {:?}", c.size(), c.first());
}
fn main() {
let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] };
let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist);
describe(&scores);
}

输出:

共 2 个元素,第一个是 Some("Intro")
共 3 个元素,第一个是 Some(90)
▶ 动手试一试

在 trait 内部,Self::Item 的意思是“这个实现选定的那个类型”。describe 里的 C::Item 也是同样的意思,只不过针对的是类型 C。(where 那一行只是把约束换个更整齐的地方写,这一页最后会细讲。)

为什么不用泛型参数?你当然可以写成 trait Container<T>。但这样一来,同一个类型就能实现它好多次:Container<String> 和 Container<u32> 都行,每个调用者还得说明自己要的是哪一个。对播放列表来说这毫无意义:它装的就是歌曲,没有别的可能。

Iterator 用 type Item 正是这个原因。Vec<i32> 的迭代器只会产出 i32,不会是别的,所以写 for x in v.iter() 时从来不需要类型注解。一个简单的经验法则:

  • 每个类型只有一个自然的答案 → 用关联类型(Iterator::Item、Add::Output)。
  • 每个类型可以有多个合理的答案 → 用泛型参数(From<T>:一个 String 可以由 &str、char、Box<str> 等等构造出来)。

在常用 trait 那一页,你通过实现 Add 让 Point 支持了 +。下面是 Add 在标准库里真正的定义:

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

这里同时用到了两个特性。Output 是关联类型(+ 运算的结果)。而 Rhs = Self 是默认类型参数(default type parameter),意思是“除非你另外指定,否则右边的类型和左边相同”。写 impl Add for Point 时用的就是默认值;想加上一个不同的类型时,就写 impl Add<OtherType> for Point。

use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Centimeters(f64);
// 使用默认的 Rhs = Self,所以这是 Meters + Meters
impl Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
// 覆盖默认值:Meters + Centimeters
impl Add<Centimeters> for Meters {
type Output = Meters;
fn add(self, other: Centimeters) -> Meters {
Meters(self.0 + other.0 / 100.0)
}
}
fn main() {
let a = Meters(2.0);
let b = Meters(3.5);
let c = Centimeters(50.0);
println!("{:?}", a + b);
println!("{:?}", a + c);
}

输出:

Meters(5.5)
Meters(2.5)
▶ 动手试一试

注意,Add 两种工具都用上了:右边的类型用泛型参数(答案有多个,可以加 Meters,也可以加 Centimeters),结果用关联类型(每一对类型只有一个答案)。默认类型参数主要在你自己设计 trait 时有用:常见情况写起来很短,少见情况也不会被堵死。

supertrait:“你还必须实现这个”

Section titled “supertrait:“你还必须实现这个””

有时候,你的 trait 只有在类型已经具备某种能力时才说得通。supertrait(父 trait)的意思是“要实现我的 trait,你必须先实现那个 trait”。写法和约束一样,用冒号:

use std::fmt;
// 实现 Pretty 的类型,也必须实现 Display
trait Pretty: fmt::Display {
fn pretty(&self) -> String {
format!("*** {} ***", self)
}
}
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
impl Pretty for Temperature {}
fn main() {
let t = Temperature { celsius: 21.456 };
println!("{}", t);
println!("{}", t.pretty());
}

输出:

21.5°C
*** 21.5°C ***
▶ 动手试一试

默认的 pretty 方法之所以能对 self 使用 {},正是因为 supertrait 保证了 Display 一定存在。如果去掉 : fmt::Display,Rust 就会拒绝那个 format!,因为它没法确定这个类型能不能打印。impl Pretty for Temperature {} 是空的,它只是在说:“好的,用默认方法就行。”

标准库里的真实例子:Copy: Clone(每个 Copy 类型也必须是 Clone),还有 Eq: PartialEq。

blanket 实现:一个 impl 覆盖很多类型

Section titled “blanket 实现:一个 impl 覆盖很多类型”

blanket 实现(blanket implementation,也叫“覆盖实现”)一次性为所有满足某个条件的类型实现一个 trait。语法是 impl<T: SomeBound> MyTrait for T:

use std::fmt::Display;
trait Shout {
fn shout(&self) -> String;
}
// 每个实现了 Display 的类型都白送一个 Shout
impl<T: Display> Shout for T {
fn shout(&self) -> String {
format!("{}!", self.to_string().to_uppercase())
}
}
fn main() {
println!("{}", "hello".shout());
println!("{}", 42.shout());
println!("{}", 'a'.shout());
println!("{}", String::from("rust is fun").shout());
}

输出:

HELLO!
42!
A!
RUST IS FUN!
▶ 动手试一试

我们从来没写过 impl Shout for i32 或 impl Shout for char,blanket 实现把它们全包了,连那些还没被写出来的 Display 类型也算在内。(这里的字符串保持英文,因为 to_uppercase() 转大写只对字母有效果。)

其实你早就用过 blanket 实现,只是自己没察觉。标准库里有一个(大致)是 impl<T: Display> ToString for T 的实现。所以每个实现了 Display 的类型都有 .to_string() 方法。同样的技巧也让你在实现 From 之后自动获得 Into。

有一个限制:blanket 实现一旦存在,你就不能再为它已经覆盖的类型添加一个更具体的实现(比如专门写一个 impl Shout for i32)。那样 Rust 会有两个答案,于是报出“conflicting implementations”(实现冲突)错误,拒绝编译。

下面这条规则,几乎每个人都会被它吓一跳。试着给 Vec<String> 实现一个好看的 Display:

use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.join(", "))
}
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> src/main.rs:3:1
|
3 | impl fmt::Display for Vec<String> {
| ^^^^^^^^^^^^^^^^^^^^^^-----------
| |
| `Vec` is not defined in the current crate
|
= note: define and implement a trait or new type instead

这就是孤儿规则(orphan rule):只有当 trait 或者类型(至少一个)定义在你自己的 crate 里时,你才能为这个类型实现这个 trait。Display 属于标准库,Vec 也属于标准库,两个都不是你的。

为什么要有这条规则?想象一下,你依赖的两个 crate 都各自写了 impl Display for Vec<String>,Rust 该用哪一个?这个问题没有好答案,所以 Rust 干脆让这种情况根本不可能出现。

绕行办法是 newtype 模式:用一个你自己的小结构体把外部类型包起来。这样这个类型就是你的了,想为它实现什么都可以:

use std::fmt;
// 包住外部类型的本地包装类型
struct Shopping(Vec<String>);
impl fmt::Display for Shopping {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let list = Shopping(vec![String::from("牛奶"), String::from("鸡蛋"), String::from("面包")]);
println!("你要买:{}", list);
println!("一共 {} 样东西", list.0.len());
}

输出:

你要买:[牛奶, 鸡蛋, 面包]
一共 3 样东西
▶ 动手试一试

self.0 取到里面的 Vec。newtype 没有任何运行时开销:在内存里,Shopping 就是一个 Vec<String>。缺点是 Vec 的方法不能直接在 Shopping 上调用,所以要写 list.0.len()(或者把你需要的方法自己加上)。newtype 的用处远不止绕过孤儿规则,详见设计模式。

完全限定语法:名字冲突的时候

Section titled “完全限定语法:名字冲突的时候”

两个 trait 可以有同名的方法,类型自己也可以有一个同名方法。那么 robot.greet() 调用的是哪一个?

trait English {
fn greet(&self) -> String;
fn language() -> String;
}
trait Spanish {
fn greet(&self) -> String;
fn language() -> String;
}
struct Robot;
impl Robot {
fn greet(&self) -> String {
String::from("BEEP BOOP")
}
}
impl English for Robot {
fn greet(&self) -> String { String::from("Hello!") }
fn language() -> String { String::from("英语") }
}
impl Spanish for Robot {
fn greet(&self) -> String { String::from("¡Hola!") }
fn language() -> String { String::from("西班牙语") }
}
fn main() {
let r = Robot;
println!("{}", r.greet()); // 类型自己的方法优先
println!("{}", English::greet(&r)); // 按名字指定 trait
println!("{}", Spanish::greet(&r));
// 这里没有 `self`,所以 Rust 需要同时知道类型和 trait:
println!("{}", <Robot as English>::language());
println!("{}", <Robot as Spanish>::language());
}

输出:

BEEP BOOP
Hello!
¡Hola!
英语
西班牙语
▶ 动手试一试

从最简短到完全明确,一共三个层次:

  • r.greet():优先选择类型自己的(固有)方法。
  • English::greet(&r):“要 English 的那个版本”。Rust 会根据 &r 推断出类型。
  • <Robot as English>::language():完全限定语法(fully qualified syntax)。没有 self 的函数必须这样写,因为没有值可以让 Rust 去推断类型。只写 English::language() 会报错:error[E0790]: cannot call associated function on trait without specifying the corresponding impl type。

这种写法你不会经常用到。真用到的时候,编译器的报错通常会直接给出正确的写法。

参数和返回值里的 impl Trait,以及 where

Section titled “参数和返回值里的 impl Trait,以及 where”

impl Trait 可以写在两个位置,含义略有不同:

  • 参数位置:fn f(x: impl Trait),由调用者决定类型。这是泛型写法 fn f<T: Trait>(x: T) 的简写。
  • 返回值位置:fn f() -> impl Trait,由函数自己选定一个具体类型,并把它藏起来。调用者只知道“这是某个实现了 Trait 的东西”。
// 调用者决定:任何产出 u32 的迭代器都行
fn print_all(items: impl Iterator<Item = u32>) {
let all: Vec<u32> = items.collect();
println!("{:?}", all);
}
// 函数决定:真实类型是一长串 Filter<...>,我们不用把它写出来
fn evens_up_to(limit: u32) -> impl Iterator<Item = u32> {
(0..=limit).filter(|n| n % 2 == 0)
}
fn main() {
print_all(vec![3, 1, 4].into_iter());
print_all(evens_up_to(10));
}

输出:

[3, 1, 4]
[0, 2, 4, 6, 8, 10]
▶ 动手试一试

返回值位置的 impl Trait 和闭包、迭代器搭配起来特别好用。evens_up_to 返回值的真实类型里包含闭包的类型,而闭包的类型根本没有名字可写。有了 impl Iterator,你就可以跳过它。

当一个函数有好几个泛型参数、每个参数又有好几个约束时,尖括号里会挤成一团。where 子句把约束挪到函数签名后面,让函数的整体样子更容易看清:

use std::fmt::{Debug, Display};
// 拥挤的写法:fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String
// 更整齐的写法:
fn report<T, U>(label: T, a: U, b: U) -> String
where
T: Display,
U: Debug + PartialOrd,
{
let bigger = if a > b { a } else { b };
format!("{}:较大的是 {:?}", label, bigger)
}
fn main() {
println!("{}", report("分数", 7, 12));
println!("{}", report(String::from("名字"), "ann", "bob"));
}

输出:

分数:较大的是 12
名字:较大的是 "bob"
▶ 动手试一试

对编译器来说,这两种写法的意思完全一样。where 也是给关联类型加约束的自然位置,比如本页第一个例子里的 C::Item: Debug。

实现了 trait,却忘了实现它的 supertrait。如果把 supertrait 例子里的 impl fmt::Display for Temperature 整块删掉,会得到:

error[E0277]: `Temperature` doesn't implement `std::fmt::Display`
--> src/main.rs:13:17
|
13 | impl Pretty for Temperature {}
| ^^^^^^^^^^^ unsatisfied trait bound
|
note: required by a bound in `Pretty`
--> src/main.rs:3:15
|
3 | trait Pretty: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `Pretty`

解决办法是把 supertrait(这里是 Display)也实现了。note 那一行正好指向要求它的 : fmt::Display。

从 -> impl Trait 返回两种不同的类型。impl Trait 只能隐藏一个类型,不能“有时是这个,有时是那个”:

fn numbers(evens: bool) -> impl Iterator<Item = u32> {
if evens {
(0..10).filter(|n| n % 2 == 0)
} else {
vec![1, 3, 5].into_iter()
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if evens {
3 | | (0..10).filter(|n| n % 2 == 0)
| | ------------------------------
| | | |
| | | the expected closure
| | expected because of this
4 | | } else {
5 | | vec![1, 3, 5].into_iter()
| | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>`
found struct `std::vec::IntoIter<{integer}>`
help: you could change the return type to be a boxed trait object
|
1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {
1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new((0..10).filter(|n| n % 2 == 0))
4 | } else {
5 ~ Box::new(vec![1, 3, 5].into_iter())
|

如果类型确实要在运行时变化,就按编译器的建议,改为返回 Box<dyn Trait>。详见 trait 对象。

  • 关联类型(type Item)让每个实现只有一个答案。如果一个类型可以用多种方式实现这个 trait,就用泛型参数。
  • 默认类型参数(如 Add<Rhs = Self>)让常见情况写起来很短;用 impl Add<Other> for T 可以覆盖默认值。
  • supertrait(trait Pretty: Display)要求先实现另一个 trait,默认方法也因此可以使用它。
  • blanket 实现(impl<T: Display> MyTrait for T)一次覆盖所有符合条件的类型,.to_string() 就是这样来的。
  • 孤儿规则禁止你为外部类型实现外部 trait。用 newtype 把类型包起来就能绕过去。
  • 名字冲突时,用完全限定语法 <Type as Trait>::function();返回值位置的 impl Trait 隐藏一个具体类型;where 让长长的约束更好读。

小测验

1. 为什么 Iterator 用 type Item,而不是 Iterator<T>?

2. trait Add<Rhs = Self> 里的 Rhs = Self 是什么意思?

3. 你想写 impl Display for Vec<String>,但 Rust 拒绝了。通常怎么解决?

4. 要调用一个没有 self 参数的函数的 English 版本,应该写哪一行?

5. 在 fn evens() -> impl Iterator<Item = u32> 中,具体的迭代器类型由谁决定?

得分: 0 / 5