当前位置: 首页 > article >正文

【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,这函数接收NewsArticleTweet两个结构体,打印:“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 ~         })
   |

报错内容就是ifelse下的返回类型是不兼容的(也就是不是同一种类型)。

使用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实现了DisplayPartialOrd的时候才会有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方法。


http://www.kler.cn/a/466972.html

相关文章:

  • GitHub 基础使用指南
  • 01、Docker学习,第一天:简单入门与安装
  • Qt之屏幕录制设计(十六)
  • 力扣66 加一
  • Python 中常见的数据结构之二推导式
  • HTML——75. 内联框架
  • 简易Type-C拉取5V/3A电流电路分享
  • 【动态重建】时间高斯分层的长体积视频
  • Excel使用VLOOKUP时注意绝对引用和相对引用区别
  • 基于Java的超级玛丽游戏的设计与实现【源码+文档+部署讲解】
  • SQLite AND/OR 运算符
  • 【信息系统项目管理师】高分论文:论信息系统项目的风险管理(数字化联合审查管理系统)
  • JVM学习指南(9)-JVM运行时数据区
  • Kotlin 协程基础知识总结六 —— 协程 Flow 的综合应用
  • rocketmq-pull模式-消费重平衡和拉取PullTaskImpl线程
  • ubuntu1604 apt镜像源切换
  • 使用PyTorch实现基于稀疏编码的生成对抗网络(GAN)在CIFAR-10数据集上的应用
  • 计算机毕业设计PyHive+Hadoop深圳共享单车预测系统 共享单车数据分析可视化大屏 共享单车爬虫 共享单车数据仓库 机器学习 深度学习
  • STM32-笔记34-4G遥控灯
  • Golang:使用minio替代文件系统实战教程
  • NLP CH3复习
  • springboot3 性能优化
  • 1.4 spring八股文学习
  • 机器学习基础例子篇
  • 如何通过 5 种有用的方法将 iPhone 连接到戴尔笔记本电脑?
  • PDF文件提示-文档无法打印-的解决办法