【Rust自学】10.4. trait Pt.2:trait作为参数和返回类型、trait bound
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
说句题外话,写这篇的时间比写所有权还还花的久,trait是真的比较难理解的概念。
10.4.1. 把trait作为参数
继续以上一篇文章中所讲的内容作为例子:
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
上的summerize
方法的返回值。
但这里有一个问题,它接收的参数是两个结构体,怎么样实现让参数可以是两个类型呢?
我们细想一下,这两个结构体的共同点是什么?没错,它们都实现了Summary
这个trait。Rust对于这种情况提供了解决方案:
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这么写:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
这两种写法等价。
但是这种简单的写法看不出来trait bound的优势,再换一个例子。比如说,我要设计一个新的nnotify
函数,叫它notify1
吧,它接收两个参数,输出"Breaking news:"后面的内容是两个参数分别调用Summary
上的summerize
方法的返回值。
trait bound写法:
pub fn notify1<T: Summary>(item1: &T, item2: &T) {
println!("Breaking news! {} {}", item1.summarize(), item2.summarize());
}
impl trait
写法:
pub fn notify1(item1: &impl Summary, item2: &impl Summary) {
println!("Breaking news! {} {}", item1.summarize(), item2.summarize());
}
前一种的函数签名显然比后一种的要跟好写也更直观。
而实际上,impl trait
写法不过是trait bound写法的语法糖,所以impl trait
写法不适合复杂情况也确实可以理解。
那么如果这个notify
函数我需要它的参数是同时实现Display
这个trait和Summary
这个trait呢?也就是如果我有两个甚至两个以上的trait bounds该怎么写呢?
看例子:
pub fn notify_with_display<T: Summary + std::fmt::Display>(item: &T) {
println!("Breaking news! {}", item);
}
使用+
号连接各个trait bound即可。
还有一点,由于Display
不在预导入模块,所以写它的时候需要把路径写出来,也可以在代码开头先引入Display
这个trait,也就是写use std::fmt::Display
,这样就可以在写trait bound时直接写Display
:
use std::fmt::Display
pub fn notify_with_display<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item);
}
别忘了impl trait
这个语法糖哦,在这个语法糖里也是用+
连接trait bounds:
use std::fmt::Display
pub fn notify_with_display(item: &impl Summary + Display) {
println!("Breaking news! {}", item);
}
这种写法有一个缺点,如果trait bounds过多,那么写的大量约束信息就会降低这个函数签名的可读性。为了解决这个问题,Rust提供了替代语法,就是在函数签名之后使用where
字句来写trait bounds。
看个使用普通写法的写多个trait bounds:
use std::fmt::Display;
use std::fmt::Debug;
pub fn special_notify<T: Summary + Display, U: Summary + Debug>(item1: &T, item2: &U) {
format!("Breaking news! {} and {}", item1.summarize(), item2.summarize());
}
使用where
字句重写的代码:
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,
{
format!("Breaking news! {} and {}", item1.summarize(), item2.summarize());
}
这种写法跟C#很相似。
10.4.2. 把trait作为返回类型
跟作为参数一样,把trait作为返回值也可以使用impl trait
。如下例:
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支持动态派发,之后会讲。
举个例子:
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
类型和NewArticle
,这时候编译器就会报错:
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) -> 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. 泛型 中提到的比大小的代码吗?我把代码粘在这里:
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`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T{
| ++++++++++++++++++++++
在学了trait之后,是不是对这种写法和这个报错信息的理解又不同了呢?
先从报错代码来分析,报错信息是比较大小的运算符>
不能应用在类型T
上,下面的help
这行又写了考虑限制类型参数T
,再往下看下面还写到了具体的做法,就是在T
后面添加std::cmp::PartialOrd
(在trait bound里只需要写PartialOrd
,因为它在预导入模块内,所以不需要把路径写全),这实际上是一个用于实现比较大小的trait,试试按照提示来改:
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: std::cmp::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];
| +
但报的错不一样了:无法从list
里移动元素,因为list
里的T
没有实现Copy
这个trait,下边的help
说如果T
实现了Clone
这个trait,考虑克隆这个值。再下面还有一个help
,说考虑使用借用的形式。
根据以上信息,有三种解决方案:
- 为泛型添加上
Copy
这个trait - 使用克隆(得为泛型加上
Clone
这个trait) - 使用借用
该选择哪个解决方案呢?这取决于你的需求。我想要这个函数能够处理数字和字符的集合,由于数字和字符都是存储在栈内存上的,所以都实现了Copy
这个trait,那么只需要为泛型添加上Copy
这个trait就可以:
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
这个trait的思路就行不通。
那就试试克隆(得为泛型加上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
| move occurs 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
这个trait,但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
加&
。
10.4.3. 使用trait bound有条件的实现方法
在使用泛型类型参数的impl
块上使用trait boud,就可以有条件地为实现了特定trait的类型来实现方法。
看个例子:
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
具体是什么类型,在Pair
上都会有new
函数,但只有T
实现了Display
和PartialOrd
的时候才会有cmd_display
这个方法。
也可以为实现了其它trait的任意类型有条件的实现某个trait。为满足trait bound的所有类型上实现trait叫做覆盖实现(blanket implementations)
以标准库中的to_string
函数为例:
impl<T: Display> ToString for T {
// ......
}
它的意思就是对所满足display
trait的类型都实现了ToString
这个trait,这就是所谓的覆盖实现,也就是可以为任何实现了display
trait的类型调用ToString
这个trait上的方法。
以整数为例:
let s = 3.to_string();
这个操作之所以能实现是因为i32
实现了Display
trait,所以可以调用ToString
上的to_string
方法。