弄懂Rust编程中的Trait
1.定义 trait
trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 NewsArticle 用于存放发生于世界各地的新闻故事,而结构体 weibo 最多只能存放 280 个字符的内容。
我们想要创建一个名为 aggregator 的多媒体聚合库用来显示可能储存在 NewsArticle 或 weibo 实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的 summarize 方法来请求摘要。下面的代码展示了一个公有 Summary trait 的定义:
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 的签名,接着就可以在多媒体聚合库中实现这个类型了。下面的代码展示了 NewsArticle 结构体上 Summary trait 的一个实现,它使用标题、作者和创建的位置作为 summarize 的返回值。对于 Weibo 结构体,我们选择将 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 Weibo {pub username: String,pub content: String,pub reply: bool,pub reweibo: bool,
}impl Summary for Weibo {fn summarize(&self) -> String {format!("{}: {}", self.username, self.content)}
}
在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl 关键字之后,我们提供需要实现 trait 的名称,接着是 for 和需要实现 trait 的类型的名称。在 impl 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
现在库在 NewsArticle 和 Weibo 上实现了Summary trait,crate 的用户可以像调用常规方法一样调用 NewsArticle 和 Weibo 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 aggregator 库 crate 的例子:
use aggregator::{Summary, Weibo};fn main() {let wb = Weibo {username: String::from("suntiger"),content: String::from("感谢大家关注到我",),reply: false,reweibo: false,};println!("1条新微博: {}", wb.summarize());
}
打印结果如下:

其他依赖 aggregator crate 的 crate 也可以将 Summary 引入作用域以便为其自己的类型实现该 trait。实现 trait 时需要注意的一个限制是,只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 aggregator crate 的自定义类型 Weibo 实现如标准库中的 Display trait,这是因为 Weibo 类型位于 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 时,可以选择保留或重载每个方法的默认行为。
下面的代码为 Summary trait 的 summarize 方法指定一个默认的字符串值,而不是像上面代码那样只是定义方法签名:
pub trait Summary {fn summarize(&self) -> String {String::from("(读取更多...)")}
}
如果想要对 NewsArticle 实例使用这个默认实现,可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块。
虽然我们不再直接为 NewsArticle 定义 summarize 方法了,但是我们提供了一个默认实现并且指定 NewsArticle 实现 Summary trait。因此,我们仍然可以对 NewsArticle 实例调用 summarize 方法,如下所示:
let article = NewsArticle {headline: String::from("这不是梦,中国队进世界杯了!"),location: String::from("中国北京"),author: String::from("suntiger"),content: String::from("中国队的世界排名因此上升了29位.",),
};println!("新文章可用!{}", article.summarize());
这段代码执行结果如下:

为 summarize 创建默认实现并不要求对 Weibo 上的 Summary 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 Summary trait,使其具有一个需要实现的 summarize_author 方法,然后定义一个 summarize 方法,此方法的默认实现调用 summarize_author 方法:
pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(从{}读取更多内容...)", self.summarize_author())}
}
为了使用这个版本的 Summary,只需在实现 trait 时定义 summarize_author 即可:
impl Summary for Weibo {fn summarize_author(&self) -> String {format!("@{}", self.username)}
}
一旦定义了 summarize_author,我们就可以对 Weibo 结构体的实例调用 summarize 了,而 summarize 的默认实现会调用我们提供的 summarize_author 定义。因为实现了 summarize_author,Summary trait 就提供了 summarize 方法的功能,且无需编写更多的代码。
let wb = Weibo {username: String::from("suntiger"),content: String::from("中国队的世界排名因此上升了29位.",),reply: false,retweet: false,
};println!("1条新微博: {}", wb.summarize());
执行这段代码后结果如下:

注意无法从相同方法的重载实现中调用默认方法。
4.trait 作为参数
知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。上例 中为 NewsArticle 和 Weibo 类型实现了 Summary trait,用其来定义了一个函数 notify 来调用其参数 item 上的 summarize 方法,该参数是实现了 Summary trait 的某种类型。为此可以使用 impl Trait 语法,像这样:
pub fn notify(item: &impl Summary) {println!("热点新闻! {}", item.summarize());
}
对于 item 参数,我们指定了 impl 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 notify 函数体中,可以调用任何来自 Summary trait 的方法,比如 summarize。我们可以传递任何 NewsArticle 或 Weibo 的实例来调用 notify。任何用其它如 String 或 i32 的类型调用该函数的代码都不能编译,因为它们没有实现 Summary。
5.Trait Bound 语法
impl Trait 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 trait bound,它看起来像:
pub fn notify<T: Summary>(item: &T) {println!("热点新闻! {}", 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 {Weibo {username: String::from("suntiger"),content: String::from("中国队的世界排名因此上升了29位.",),reply: false,reweibo: false,}
}
通过使用 impl Summary 作为返回值类型,我们指定了 returns_summarizable 函数返回某个实现了 Summary trait 的类型,但是不确定其具体的类型。在这个例子中 returns_summarizable 返回了一个 Weibo,不过调用方并不知情。
返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第十三章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。impl Trait 允许你简单的指定函数返回一个 Iterator 而无需写出实际的冗长的类型。
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle 或 Weibo 就行不通:
fn returns_summarizable(switch: bool) -> impl Summary {if switch {NewsArticle {headline: String::from("这不是梦,中国队进世界杯了!",),location: String::from("中国北京"),author: String::from("suntiger"),content: String::from("中国队有史以来第二次闯进世界杯.",),}} else {Weibo {username: String::from("suntiger"),content: String::from("中国队的世界排名因此上升了29位.",),reply: false,reweibo: false,}}
}
这里尝试返回 NewsArticle 或 Weibo。但不能编译,因为 impl Trait 工作方式的限制。
9.使用 trait bound 有条件地实现方法
通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,前面例子 中的类型 Pair<T> 总是实现了 new 方法并返回一个 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 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。
相关文章:
弄懂Rust编程中的Trait
1.定义 trait trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。 一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话ÿ…...
关于登山扣亚马逊\SHEIN出口合规标准ASTM F1774 指南解析
登山的时候配合绳子起到一个承重悬挂的作用.采用铝合金、铁或者是不锈钢等材料制作而成的一种登山工具之一。 其形状多样,比较常见的是椭圆形和圆形的,除此之外还有长方形、三角形等样式的登山扣。铝合金登山扣由于质地较轻所以重量也比较轻,…...
浅析ChatGPT中涉及到的几种技术点
❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…...
Web实战:基于Django与Bootstrap的在线计算器
文章目录 写在前面实验目标实验内容1. 创建项目2. 导入框架3. 配置项目前端代码后端代码 4. 运行项目 注意事项写在后面 写在前面 本期内容:基于Django与Bootstrap的在线计算器 实验环境: vscodepython(3.11.4)django(4.2.7)bootstrap(3.4.1)jquery(3…...
曲率半径的推导
参考文章 参考文章...
0时区格林威治时间转换手机当地时间-Android
假设传入的是2023-11-01T12:59:10.420987这样的格式 要将格式为2023-11-01T12:59:10.420987的UTC时间字符串转换为Android设备本地时间,您可以使用java.time包中的类(在API 26及以上版本中可用)。如果您的应用需要支持较低版本的Android&…...
git-3
1.如何让工作区的文件恢复为和暂存区一样? 工作区所作的变更还不及暂存区的变更好,想从暂存区拷贝到工作区,变更工作区(恢复成和暂存区一样的状态),想到用git checkout -- 文件名 2.怎样取消暂存区部分文件的更改? 如…...
【python爬虫】scrapy在pycharm 调试
scrapy在pycharm 调试 1、使用scrapy创建一个项目 scrapy startproject tutorial 2、在朋友pycharm中调试scrapy 2.1 通过文件run.py调试 在根目录下新建一个文件run.py(与scrapy.cfg文件的同一目录下), debug ‘run’即可 # -*- coding:utf-8 -*- from scrapy import c…...
yoloV5模型中,x,s,n,m,l之间区别
避免误导大家,从小到大顺序为:n,s,m,l,x YOLOv5 的不同变体(如 YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x 和 YOLOv5n)表示不同大小和复杂性的模型。这些变体在速度和准确度之间提供了不同的权衡,以适应不同的计算能力和实时性需求。下面简要介绍这些变体的区别: YOLOv5s:这…...
RabbitMQ快速入门(简单收发消息)
文章目录 前言一、数据隔离1.用户管理2.virtual host 二、控制台收发1.交换机2.队列3.绑定 三、编程式收发1.依赖和配置2.收发信息 总结 前言 1.了解数据隔离 2.RabbitMQ控制台收发信息 3.SpringBoot整合RabbitMQ收发信息 一、数据隔离 1.用户管理 点击Admin选项卡࿰…...
java面试-zookeeper
1、什么是zap协议 ZAB 协议总共包含以下两部分内容: ZAB 协议通过两阶段提交的方式来确保分布式系统的一致性。这两阶段分别是:准备阶段和提交阶段。在准备阶段,一个节点(称为 Leader)向其他节点(称为 Fol…...
VBA技术资料MF85:将工作簿批量另存为PDF文件
我给VBA的定义:VBA是个人小型自动化处理的有效工具。利用好了,可以大大提高自己的工作效率,而且可以提高数据的准确度。我的教程一共九套,分为初级、中级、高级三大部分。是对VBA的系统讲解,从简单的入门,到…...
大数据-计算框架选型与对比
计算框架选型与对比 一、大数据平台二、计算框架分类1.批处理架构2.实时流处理架构3.流批一体处理架构 三、计算框架关键指标1.处理模式2.可伸缩性3.消息传递3.1 至少一次(at least once)3.2 至多一次(ai most once)3.3 恰好一次&…...
2023亚太杯数学建模C题思路 - 我国新能源电动汽车的发展趋势
1 赛题 问题C 我国新能源电动汽车的发展趋势 新能源汽车是指以先进技术原理、新技术、新结构的非常规汽车燃料为动力来源( 非常规汽车燃料指汽油、柴油以外的燃料),将先进技术进行汽车动力控制和驱动相结 合的汽车。新能源汽车主要包括四种类型&#x…...
【02】ES6:let 和 const 命令
一、let 命令 ES6 新增了 let 命令,用来声明变量。相对于 var 命令,有以下特点。 1、存在块级作用域 let 拥有块级作用域,声明的变量仅在块级作用域内有效。 // let 存在块级作用域,变量 a 只在当前代码块有效,在代…...
230814期就业平均薪资:8146元——转行是男人最好的医美!~
记得三个月前,很多同学在从事着跟后台网优完全不同的工作,虽然岗位不一样,但是薪资低,重复性劳动,加班多,发展受限,大家都一样身心饱受折磨......回头看,如梦般不真实,却…...
shell脚本三
目录 一、循环语句 一、循环 二、for循环语句 1.列表循环 2.与c语言循环相似的for循环 3.使用for打印三角形以及乘法表 4.测试172.16.114.0网段存活的主机并将存活的主机IP地址写入文件中,未存活的主机放入另一文件中 三、while循环语句 四、until循环语句…...
地埋式积水监测仪厂家直销推荐,致力于积水监测
地埋式积水监测仪是一种高科技设备,能够实时监测地面积水深度,并及时发出预警信息,有效避免因积水而产生的安全隐患。这种智能监测仪可以安装在城市道路、立交桥、地下车库等易积水地势较低的地方,以确保及时监测特殊地段的积水&a…...
CentOS7安装部署Kafka with KRaft
文章目录 CentOS7安装部署Kafka with KRaft一、前言1.简介2.架构3.环境 二、正文1.部署服务器2.基础环境1)主机名2)Hosts文件3)关闭防火墙4)JDK 安装部署 3.单机部署1)下载软件包2)修改配置文件3࿰…...
Java,数据结构与集合源码,关于Map接口的实现类(HashMap、LinkedHashMap)
HashMap中的元素的特点: HashMap中的所有key之间是不可重复的、无序的。所有的key构成一个Set集合。 HashMap中的所有的value彼此之间是可重复的、无序的。所有的value构成一个Collection集合。 HashMap中的一对key-value,就构成了一个entry。Map中的ent…...
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...
Spring Boot + MyBatis 集成支付宝支付流程
Spring Boot MyBatis 集成支付宝支付流程 核心流程 商户系统生成订单调用支付宝创建预支付订单用户跳转支付宝完成支付支付宝异步通知支付结果商户处理支付结果更新订单状态支付宝同步跳转回商户页面 代码实现示例(电脑网站支付) 1. 添加依赖 <!…...
Java并发编程实战 Day 11:并发设计模式
【Java并发编程实战 Day 11】并发设计模式 开篇 这是"Java并发编程实战"系列的第11天,今天我们聚焦于并发设计模式。并发设计模式是解决多线程环境下常见问题的经典解决方案,它们不仅提供了优雅的设计思路,还能显著提升系统的性能…...
GraphRAG优化新思路-开源的ROGRAG框架
目前的如微软开源的GraphRAG的工作流程都较为复杂,难以孤立地评估各个组件的贡献,传统的检索方法在处理复杂推理任务时可能不够有效,特别是在需要理解实体间关系或多跳知识的情况下。先说结论,看完后感觉这个框架性能上不会比Grap…...
Java中HashMap底层原理深度解析:从数据结构到红黑树优化
一、HashMap概述与核心特性 HashMap作为Java集合框架中最常用的数据结构之一,是基于哈希表的Map接口非同步实现。它允许使用null键和null值(但只能有一个null键),并且不保证映射顺序的恒久不变。与Hashtable相比,Hash…...
比特币:固若金汤的数字堡垒与它的四道防线
第一道防线:机密信函——无法破解的哈希加密 将每一笔比特币交易比作一封在堡垒内部传递的机密信函。 解释“哈希”(Hashing)就是一种军事级的加密术(SHA-256),能将信函内容(交易细节…...
