Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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,它以 NewsArticleTweet 这两种类型作为参数,并打印 Breaking news!,后面跟上在参数上调用 Summarysummarize 方法的返回值,就会遇到一个问题:

这个函数接收的是两个不同的结构体类型。怎样才能让参数同时适用于这两种类型呢?

细想一下:这两个结构体有什么共同点?没错——它们都实现了 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 时,item1item2 必须是同一个具体类型(都是 &T)。用 impl Trait 时,只要各自都实现了 Summaryitem1item2 可以是不同类型(例如一个是 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 的值,可能有两种返回类型:TweetNewsArticle。这时编译器会报错:

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 ~         })
   |

报错信息说的是 ifelse 的返回类型不兼容,也就是它们不是同一种类型。

使用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,建议使用借用。

根据以上信息,有三种解决方案:

  • 为泛型类型添加 Copy trait
  • 使用克隆,也就是为泛型类型添加 Clone trait
  • 使用借用

该选择哪个方案呢?这取决于你的需求。我想让这个函数处理数字和字符的集合。由于数字和字符都存储在栈上,它们都实现了 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] 前面加 & 使其成为引用。另外,比较时两边应是同一种值:这里 itemlargest 都是 &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 同时实现了 DisplayPartialOrd 时,才会有 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 方法。