10.4 trait Pt.2:trait作为参数和返回类型、trait bound
10.4.1. 把trait作为参数
继续以 10.3 trait Pt.1:trait的定义、约束与实现 中的内容为例:
#![allow(unused)]
fn main() {
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
}
如果我们新定义一个函数 notify,它以 NewsArticle 和 Tweet 这两种类型作为参数,并打印 Breaking news!,后面跟上在参数上调用 Summary 中 summarize 方法的返回值,就会遇到一个问题:
这个函数接收的是两个不同的结构体类型。怎样才能让参数同时适用于这两种类型呢?
细想一下:这两个结构体有什么共同点?没错——它们都实现了 Summary trait。Rust 为这种情况提供了解决方案:
#![allow(unused)]
fn main() {
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
}
只要把参数类型写成 impl 某个trait 即可。因为这两个结构体都实现了 Summary trait,所以写成 impl Summary。又因为这个函数不需要数据的所有权,所以写成引用:&impl Summary。如果还有其他数据类型也实现了 Summary,同样可以作为参数传入。
impl trait 语法适用于简单情况。对于更复杂的情况,通常使用 trait bound 语法。
同样的代码,用 trait bound 来写:
#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
}
这两种写法是等价的。
不过,在有两个参数时,这两种写法的差别会更明显。假设我要设计一个新的 notify1 函数。它接收两个参数,Breaking news! 后面的内容是分别在每个参数上调用 summarize 的返回值。
trait bound 写法:
#![allow(unused)]
fn main() {
pub fn notify1<T: Summary>(item1: &T, item2: &T) {
println!("Breaking news! {} {}", item1.summarize(), item2.summarize());
}
}
impl trait 写法:
#![allow(unused)]
fn main() {
pub fn notify1(item1: &impl Summary, item2: &impl Summary) {
println!("Breaking news! {} {}", item1.summarize(), item2.summarize());
}
}
显然,这两种写法并不等价。用 trait bound 时,item1 和 item2 必须是同一个具体类型(都是 &T)。用 impl Trait 时,只要各自都实现了 Summary,item1 和 item2 可以是不同类型(例如一个是 NewsArticle,另一个是 Tweet)。当你需要两个参数共用同一类型时,用 trait bound;当允许不同类型、且签名仍然简单时,用 impl Trait 即可。
在简单情况下,impl Trait 相当于带有 trait bound 的匿名泛型,写起来更省事。对于更复杂的签名——例如多个参数必须是同一类型,或约束很多——具名的 trait bound(或 where 子句)通常更清晰。
那么,如果 notify 函数需要其参数同时实现 Display trait 和 Summary trait 呢?换句话说,两个或更多 trait bound 该怎么写?
例如:
#![allow(unused)]
fn main() {
pub fn notify_with_display<T: Summary + std::fmt::Display>(item: &T) {
println!("Breaking news! {}", item);
}
}
使用 + 连接各个 trait bound。
还有一点:因为 Display 不在预导入模块中,写它时需要写出完整路径。也可以先在代码开头引入 Display,像这样:use std::fmt::Display。然后就可以在 trait bound 中直接写 Display:
#![allow(unused)]
fn main() {
use std::fmt::Display;
pub fn notify_with_display<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item);
}
}
别忘了,impl trait 也是语法糖,在这种语法糖中同样用 + 连接 trait bound:
#![allow(unused)]
fn main() {
use std::fmt::Display;
pub fn notify_with_display(item: &(impl Summary + Display)) {
println!("Breaking news! {}", item);
}
}
这种写法有一个缺点:如果 trait bound 太多,大量约束信息会降低函数签名的可读性。为了解决这个问题,Rust 提供了一种替代语法:在函数签名之后使用 where 子句来写 trait bound。
下面是多个 trait bound 的普通写法:
#![allow(unused)]
fn main() {
use std::fmt::Display;
use std::fmt::Debug;
pub fn special_notify<T: Summary + Display, U: Summary + Debug>(item1: &T, item2: &U) {
println!("Breaking news! {} and {}", item1.summarize(), item2.summarize());
}
}
同样的代码用 where 子句重写:
#![allow(unused)]
fn main() {
use std::fmt::Display;
use std::fmt::Debug;
pub fn special_notify<T, U>(item1: &T, item2: &U)
where
T: Summary + Display,
U: Summary + Debug,
{
println!("Breaking news! {} and {}", item1.summarize(), item2.summarize());
}
}
这种语法与 C# 很相似。
10.4.2. 把trait作为返回类型
和把 trait 作为参数一样,把 trait 作为返回值也可以使用 impl trait。例如:
#![allow(unused)]
fn main() {
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
这种语法有一个缺点:如果返回类型实现了某个 trait,那么必须保证这个函数/方法所有可能的返回值都只能是同一种类型。这是因为 impl 形式在工作方式上有一些限制,所以 Rust 并非在所有情况下都支持它。但 Rust 支持动态派发,之后会讲。
例如:
#![allow(unused)]
fn main() {
fn returns_summarizable(flag:bool) -> impl Summary {
if flag {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
} else {
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh, Scotland"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
}
}
}
根据 flag 的值,可能有两种返回类型:Tweet 和 NewsArticle。这时编译器会报错:
error[E0308]: `if` and `else` have incompatible types
--> src/lib.rs:42:9
|
32 | / if flag {
33 | | / Tweet {
34 | | | username: String::from("horse_ebooks"),
35 | | | content: String::from(
36 | | | "of course, as you probably already know, people",
... | |
39 | | | retweet: false,
40 | | | }
| | |_________- expected because of this
41 | | } else {
42 | | / NewsArticle {
43 | | | headline: String::from("Penguins win the Stanley Cup Championship!"),
44 | | | location: String::from("Pittsburgh, PA, USA"),
45 | | | author: String::from("Iceburgh, Scotland"),
... | |
49 | | | ),
50 | | | }
| | |_________^ expected `Tweet`, found `NewsArticle`
51 | | }
| |_______- `if` and `else` have incompatible types
|
help: you could change the return type to be a boxed trait object
|
31 - fn returns_summarizable(flag:bool) -> impl Summary {
31 + fn returns_summarizable(flag:bool) -> Box<dyn Summary> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
33 ~ Box::new(Tweet {
34 | username: String::from("horse_ebooks"),
...
39 | retweet: false,
40 ~ })
41 | } else {
42 ~ Box::new(NewsArticle {
43 | headline: String::from("Penguins win the Stanley Cup Championship!"),
...
49 | ),
50 ~ })
|
报错信息说的是 if 和 else 的返回类型不兼容,也就是它们不是同一种类型。
使用trait bounds的实例
还记得在 10.2. 泛型 中提到的比大小代码吗?我把它粘在这里:
#![allow(unused)]
fn main() {
fn largest<T>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list{
if item > largest{
largest = item;
}
}
largest
}
}
当时出现的错误我也粘在这里:
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:4:17
|
4 | if item > largest{
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T{
| ++++++++++++++++++++++
现在学了 trait 之后,对这段代码及其报错信息的理解是不是又不一样了?
先从报错信息开始分析。错误说比较运算符 > 不能应用于类型 T。下面的 help 行说考虑限制类型参数 T,再往下给出了具体做法:在 T 后面加上 std::cmp::PartialOrd(在 trait bound 中只需要写 PartialOrd,因为它在预导入模块中,所以不需要写完整路径)。这实际上就是用于比较的 trait。试试按照提示修改:
#![allow(unused)]
fn main() {
fn largest<T: PartialOrd>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list{
if item > largest{
largest = item;
}
}
largest
}
}
仍然会报错:
error[E0508]: cannot move out of type `[T]`, a non-copy slice
--> src/main.rs:2:23
|
2 | let mut largest = list[0];
| ^^^^^^^
| |
| cannot move out of here
| move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
|
help: if `T` implemented `Clone`, you could clone the value
--> src/main.rs:1:12
|
1 | fn largest<T: PartialOrd>(list: &[T]) -> T{
| ^ consider constraining this type parameter with `Clone`
2 | let mut largest = list[0];
| ------- you could clone this value
help: consider borrowing here
|
2 | let mut largest = &list[0];
| +
error[E0507]: cannot move out of a shared reference
--> src/main.rs:3:18
|
3 | for &item in list{
| ---- ^^^^
| |
| data moved here because `item` has type `T`, which does not implement the `Copy` trait
|
help: consider removing the borrow
|
3 - for &item in list{
3 + for item in list{
|
但这次错误不同了:无法从 list 中移出元素,因为 list 中的 T 没有实现 Copy trait。下面的 help 说如果 T 实现了 Clone trait,可以考虑克隆该值。再下面还有一个 help,建议使用借用。
根据以上信息,有三种解决方案:
- 为泛型类型添加
Copytrait - 使用克隆,也就是为泛型类型添加
Clonetrait - 使用借用
该选择哪个方案呢?这取决于你的需求。我想让这个函数处理数字和字符的集合。由于数字和字符都存储在栈上,它们都实现了 Copy trait,所以只要给泛型类型加上 Copy 就够了:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list{
if item > largest{
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
输出:
The largest number is 100
The largest char is y
如果我想让这个函数比较 String 集合呢?由于 String 存储在堆上,它没有实现 Copy trait,所以给泛型类型加上 Copy 的思路行不通。
那就试试克隆,也就是给泛型类型加上 Clone trait:
fn largest<T: PartialOrd + Clone>(list: &[T]) -> T{
let mut largest = list[0].clone();
for &item in list.iter() {
if item > largest{
largest = item;
}
}
largest
}
fn main() {
let string_list = vec![String::from("dev1ce"), String::from("Zywoo")];
let result = largest(&string_list);
println!("The largest string is {}", result);
}
输出:
error[E0507]: cannot move out of a shared reference
--> src/main.rs:3:18
|
3 | for &item in list.iter() {
| ---- ^^^^^^^^^^^
| |
| data moved here because `item` has type `T`, which does not implement the `Copy` trait
|
help: consider removing the borrow
|
3 - for &item in list.iter() {
3 + for item in list.iter() {
|
错误说无法移动数据,因为这种写法要求实现 Copy,而 String 做不到。该怎么办呢?
那就不要移动数据,不要使用模式匹配。去掉 item 前面的 &,这样 item 就从 T 变成了不可变引用 &T。然后在比较时使用解引用运算符 *,把 &T 解引用为 T 再与 largest 比较(下面的代码就是这种做法),或者在 largest 前面加 & 使其变成 &T。总之,被比较的两个值必须类型一致:
fn largest<T: PartialOrd + Clone>(list: &[T]) -> T{
let mut largest = list[0].clone();
for item in list.iter() {
if *item > largest{
largest = item.clone();
}
}
largest
}
fn main() {
let string_list = vec![String::from("dev1ce"), String::from("Zywoo")];
let result = largest(&string_list);
println!("The largest string is {}", result);
}
记住 T 没有实现 Copy trait,所以给 largest 赋值时需要使用 clone 方法。
输出:
The largest string is dev1ce
之所以这样写,是因为返回值是 T。如果把返回值改成 &T,就不再需要克隆了:
fn largest<T: PartialOrd>(list: &[T]) -> &T{
let mut largest = &list[0];
for item in list.iter() {
if item > largest{
largest = item;
}
}
largest
}
fn main() {
let string_list = vec![String::from("dev1ce"), String::from("Zywoo")];
let result = largest(&string_list);
println!("The largest string is {}", result);
}
但要记住,初始化 largest 时必须把它设为 &T,所以需要在 list[0] 前面加 & 使其成为引用。另外,比较时两边应是同一种值:这里 item 和 largest 都是 &T,因此可以直接写 item > largest。
10.4.3. 使用trait bound有条件地实现方法
如果在带有泛型类型参数的 impl 块上使用 trait bound,就可以有条件地为实现了特定 trait 的类型实现方法。
例如:
#![allow(unused)]
fn main() {
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
}
无论 T 的具体类型是什么,new 函数都会存在于 Pair 上。但只有当 T 同时实现了 Display 和 PartialOrd 时,才会有 cmp_display 方法。
也可以为实现了另一个 trait 的任意类型有条件地实现某个 trait。为所有满足某个 trait bound 的类型实现一个 trait,叫做覆盖实现(blanket implementation)。
以标准库中的 to_string 函数为例:
#![allow(unused)]
fn main() {
impl<T: Display> ToString for T {
// ......
}
}
这意味着对所有满足 Display trait 的类型都实现了 ToString,这就是覆盖实现:任何实现了 Display 的类型都可以调用 ToString 上的方法。
以整数为例:
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
之所以能这样做,是因为 i32 实现了 Display trait,所以可以调用 ToString 上的 to_string 方法。