泛型、Trait 和生命周期(中)

目录

1、定义 trait

2、为类型实现 trait

3、默认实现

4、trait 作为参数 

5、Trait Bound 语法

6、通过 + 指定多个 trait bound

7、通过 where 简化 trait bound

8、返回实现了 trait 的类型

9、使用 trait bound 有条件地实现方法


trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共同行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

注意:trait 类似于其他语言中的常被称为 接口interfaces)的功能,虽然有一些不同。

1、定义 trait

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

例如,这里有多个存放了不同类型和属性文本的结构体:结构体 NewsArticle 用于存放发生于世界各地的新闻故事,而结构体 Tweet 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。

我们想要创建一个名为 aggregator 的多媒体聚合库用来显示可能储存在 NewsArticle 或 Tweet 实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的 summarize 方法来请求摘要。

pub trait Summary {fn summarize(&self) -> String;
}

 

这里使用 trait 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 Summary。我们也声明 trait 为 pub 以便依赖这个 crate 的 crate 也可以使用这个 trait,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 fn summarize(&self) -> String

在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 Summary trait 的类型都拥有与这个签名的定义完全一致的 summarize 方法。

trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。

2、为类型实现 trait

现在我们定义了 Summary trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13 中展示了 NewsArticle 结构体上 Summary trait 的一个实现,它使用标题、作者和创建的位置作为 summarize 的返回值。对于 Tweet 结构体,我们选择将 summarize 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。

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

在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl 关键字之后,我们提供需要实现 trait 的名称,接着是 for 和需要实现 trait 的类型的名称。在 impl 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。

现在库在 NewsArticle 和 Tweet 上实现了Summary trait,crate 的用户可以像调用常规方法一样调用 NewsArticle 和 Tweet 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 aggregator 库 crate 的例子:

pub mod aggregator;
use aggregator::{Summary, Tweet};
fn main() {let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,};println!("1 new tweet: {}", tweet.summarize());
}

其他依赖 aggregator crate 的 crate 也可以将 Summary 引入作用域以便为其自己的类型实现该 trait。需要注意的限制是,只有在 trait 或类型至少有一个属于当前 crate 时,我们才能对类型实现该 trait。例如,可以为 aggregator crate 的自定义类型 Tweet 实现如标准库中的 Display trait,这是因为 Tweet 类型位于 aggregator crate 本地的作用域中。类似地,也可以在 aggregator crate 中为 Vec<T> 实现 Summary,这是因为 Summary trait 位于 aggregator crate 本地作用域中。

但是不能为外部类型实现外部 trait。例如,不能在 aggregator crate 中为 Vec<T> 实现 Display trait。这是因为 Display 和 Vec<T> 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性coherence)的程序属性的一部分,或者更具体的说是 孤儿规则orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

3、默认实现

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}
}

如果想要对 NewsArticle 实例使用这个默认实现,可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块。

虽然我们不再直接为 NewsArticle 定义 summarize 方法了,但是我们提供了一个默认实现并且指定 NewsArticle 实现 Summary trait。因此,我们仍然可以对 NewsArticle 实例调用 summarize 方法,如下所示:

    let article = NewsArticle {headline: String::from("Penguins win the Stanley Cup Championship!"),location: String::from("Pittsburgh, PA, USA"),author: String::from("Iceburgh"),content: String::from("The Pittsburgh Penguins once again are the best \hockey team in the NHL.",),};println!("New article available! {}", article.summarize());

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 Summary trait,使其具有一个需要实现的 summarize_author 方法,然后定义一个 summarize 方法,此方法的默认实现调用 summarize_author 方法:

pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(Read more from {}...)", self.summarize_author())}
}

为了使用这个版本的 Summary,只需在实现 trait 时定义 summarize_author 即可:

impl Summary for Tweet {fn summarize_author(&self) -> String {format!("@{}", self.username)}
}

 一旦定义了 summarize_author,我们就可以对 Tweet 结构体的实例调用 summarize 了,而 summarize 的默认实现会调用我们提供的 summarize_author 定义。因为实现了 summarize_authorSummary trait 就提供了 summarize 方法的功能,且无需编写更多的代码。

    let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people",),reply: false,retweet: false,};println!("1 new tweet: {}", tweet.summarize());

 

注意:无法从相同方法的重载实现中调用默认方法。

4、trait 作为参数 

知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。上面示例中为 NewsArticle 和 Tweet 类型实现了 Summary trait,用其来定义了一个函数 notify 来调用其参数 item 上的 summarize 方法,该参数是实现了 Summary trait 的某种类型。为此可以使用 impl Trait 语法,像这样: 

pub fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());
}

对于 item 参数,我们指定了 impl 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 notify 函数体中,可以调用任何来自 Summary trait 的方法,比如 summarize。我们可以传递任何 NewsArticle 或 Tweet 的实例来调用 notify。任何用其它如 String 或 i32 的类型调用该函数的代码都不能编译,因为它们没有实现 Summary。 

5、Trait Bound 语法

impl Trait 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 trait bound,它看起来像: 

pub fn notify<T: Summary>(item: &T) {println!("Breaking news! {}", item.summarize());
}

这与之前的例子相同,不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。

impl Trait 很方便,适用于短小的例子。更长的 trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 Summary 的参数。使用 impl Trait 的语法看起来像这样:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

这适用于 item1 和 item2 允许是不同类型的情况(只要它们都实现了 Summary)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

泛型 T 被指定为 item1 和 item2 的参数限制,如此传递给参数 item1 和 item2 值的具体类型必须一致。

6、通过 + 指定多个 trait bound

如果 notify 需要显示 item 的格式化形式,同时也要使用 summarize 方法,那么 item 就需要同时实现两个不同的 trait:Display 和 Summary。这可以通过 + 语法实现:

pub fn notify(item: &(impl Summary + Display)) {

+ 语法也适用于泛型的 trait bound:

pub fn notify<T: Summary + Display>(item: &T) {

通过指定这两个 trait bound,notify 的函数体可以调用 summarize 并使用 {} 来格式化 item

7、通过 where 简化 trait bound

然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 where 从句中指定 trait bound 的语法。所以除了这么写:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

还可以像这样使用 where 从句:

fn some_function<T, U>(t: &T, u: &U) -> i32
whereT: Display + Clone,U: Clone + Debug,
{

这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。 

8、返回实现了 trait 的类型

也可以在返回值中使用 impl Trait 语法,来返回实现了某个 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,}
}

通过使用 impl Summary 作为返回值类型,我们指定了 returns_summarizable 函数返回某个实现了 Summary trait 的类型,但是不确定其具体的类型。在这个例子中 returns_summarizable 返回了一个 Tweet,不过调用方并不知情。

返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第十三章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。impl Trait 允许你简单的指定函数返回一个 Iterator 而无需写出实际的冗长的类型。

不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:

fn returns_summarizable(switch: bool) -> impl Summary {if switch {NewsArticle {headline: String::from("Penguins win the Stanley Cup Championship!",),location: String::from("Pittsburgh, PA, USA"),author: String::from("Iceburgh"),content: String::from("The Pittsburgh Penguins once again are the best \hockey team in the NHL.",),}} else {Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people",),reply: false,retweet: false,}}
}

这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。 

9、使用 trait bound 有条件地实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。Self 是一个 impl 块类型的类型别名(type alias),在这里是 Pair<T>)。不过在下一个 impl 块中,只有那些为 T 类型实现了 PartialOrd trait(来允许比较)  Display trait(来启用打印)的 Pair<T> 才会实现 cmp_display 方法:

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

也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,它们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。这个 impl 块看起来像这样: 

impl<T: Display> ToString for T {// --snip--
}

因为标准库有了这些 blanket implementation,我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display: 

let s = 3.to_string();

blanket implementation 会出现在 trait 文档的 “Implementers” 部分。

trait 和 trait bound 让我们能够使用泛型类型参数来减少重复,而且能够向编译器明确指定泛型类型需要拥有哪些行为。然后编译器可以利用 trait bound 信息检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个未定义的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/457400.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Powershell Install 一键部署Openssl+certificate证书创建

前言 Openssl 是一个方便的实用程序,用于创建自签名证书。您可以在所有操作系统(如 Windows、MAC 和 Linux 版本)上使用 OpenSSL。 Windows openssl 下载 前提条件 开启wmi,配置网卡,参考 自签名证书 创建我们自己的根 CA 证书和 CA 私钥(我们自己充当 CA)创建服务器…

[每周一更]-(第85期):NLP-实战操作-文本分类

NLP文本分类的应用场景 医疗领域 - 病历自动摘要&#xff1a; 应用&#xff1a; 利用NLP技术从医疗文档中自动生成病历摘要&#xff0c;以帮助医生更快速地了解患者的状况。 法律领域 - 法律文件分类&#xff1a; 应用&#xff1a; 使用文本分类技术自动分类法律文件&#xf…

虚拟飞控计算机:飞行控制系统验证与优化的利器

01.背景介绍 随着航空技术的飞速发展&#xff0c;飞行控制系统作为飞机的心脏&#xff0c;全面负责监测、调整和维持飞行器的姿态、航向、高度等参数&#xff0c;用以确保飞行的安全和稳定。为了满足这些要求&#xff0c;现代飞控系统通常采用先进的处理器和外设来确保其高效、…

微服务-微服务Alibaba-Nacos 源码分析 (源码流程图)-2.0.1

客户端注册临时实例&#xff0c;GRPC处理 客户端服务发现 及订阅处理

SpringCloud-创建多模块项目

在微服务架构中&#xff0c;项目的组织结构对于代码的维护和团队的协作至关重要。Spring Cloud作为一个强大的微服务框架&#xff0c;提供了丰富的功能和组件&#xff0c;同时也支持多模块项目的创建&#xff0c;使得代码结构更加清晰、易于管理。本文将介绍如何使用 Spring Cl…

LeetCode:26.删除有序数组中的重复项

26. 删除有序数组中的重复项 - 力扣&#xff08;LeetCode&#xff09; 目录 题目&#xff1a; 思路&#xff1a; 代码注释&#xff1a; 每日表情包&#xff1a; 题目&#xff1a; 思路&#xff1a; 没啥特殊的&#xff0c;老老实实双指针遍历数组&#xff0c;&#xff0…

JavaScript入门学习

JavaScript 一.什么是JavaScript与作用 JavaScript&#xff08;简称“JS”&#xff09; 是一种具有函数优先的轻量级&#xff0c;解释型或即时编译型的编程语言 嵌入HTML中&#xff0c;与Css一样。对浏览器事件作出响应操作HTML元素及节点。可以动态操作CSS样式。在数据被提交…

mac协议远程管理软件:Termius for Mac 8.4.0激活版

Termius是一款远程访问和管理工具&#xff0c;旨在帮助用户轻松地远程连接到各种服务器和设备。它适用于多种操作系统&#xff0c;包括Windows、macOS、Linux和移动设备。 该软件提供了一个直观的界面&#xff0c;使用户可以通过SSH、Telnet和Mosh等协议连接到远程设备。它还支…

【iOS分类、关联对象】如何使用关联对象给分类实现一个weak的属性

如何使用关联对象给分类实现一个weak的属性 通过关联对象objc_setAssociatedObject中的策略policy可知&#xff0c;并不支持使用weak修饰对象属性&#xff1a; typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {OBJC_ASSOCIATION_ASSIGN 0, //assignOBJC_ASSOCIATION…

数据结构第十天(排序算法总结)

目录 前言 常数时间复杂度&#xff1a;O(1) 线性时间复杂度&#xff1a;O(n) 线性对数时间复杂度&#xff1a;O(n log n) 平方时间复杂度&#xff1a;O(n^2) 对数时间复杂度&#xff1a;O(log n) 前言 排序算法的学习可以告一段落了。但算法的学习永不停止。 今天&…

【Git版本控制 01】基本操作

目录 一、初始配置 二、添加文件 三、查看日志 四、修改文件 五、版本回退 六、撤销修改 七、删除文件 一、初始配置 Git版本控制器&#xff1a;记录每次的修改以及版本迭代的一个管理系统。 # 初始化本地仓库&#xff1a;git init(base) [rootlocalhost gitcode]# gi…

作业:单身狗1

思路&#xff1a; 一&#xff1a;题目一开始就规定了这个数组的标准——只有一个数字出现一次&#xff0c;其他数字都是成对出现的&#xff0c;因此&#xff0c;重点就是如何排除成对的数&#xff0c;和保留单独的数 二&#xff1a;^的特点&#xff1a;相同为0&#xff0c;不…