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>等等构造出来)。
默认类型参数与运算符
Section titled “默认类型参数与运算符”在常用 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 + Metersimpl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// 覆盖默认值:Meters + Centimetersimpl 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 的类型,也必须实现 Displaytrait 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 的类型都白送一个 Shoutimpl<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”(实现冲突)错误,拒绝编译。
孤儿规则与 newtype 绕行办法
Section titled “孤儿规则与 newtype 绕行办法”下面这条规则,几乎每个人都会被它吓一跳。试着给 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 BOOPHello!¡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) -> Stringwhere 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 this4 | | } 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
- 高级 trait — Rust 程序设计语言:关联类型、默认类型参数、supertrait 和 newtype 模式,还有更多示例。
- Traits — The Rust Reference(英文):trait 项、supertrait 和关联项的精确规则。
- Orphan rules — The Rust Reference(英文):什么情况下允许写 impl 的准确定义。
impl Trait— The Rust Reference(英文):参数位置和返回值位置的impl Trait有什么区别。- 父 trait — 通过例子学 Rust:一个简短、可运行的例子,展示建立在其他 trait 之上的 trait。