弄懂Rust编程中的Trait

news2024/11/18 17:43:01

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。我们也声明 traitpub 以便依赖这个 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 方法所拥有的行为。

现在库在 NewsArticleWeibo 上实现了Summary trait,crate 的用户可以像调用常规方法一样调用 NewsArticleWeibo 实例的 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。这是因为 DisplayVec<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_authorSummary 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 来接受多种不同类型的参数。上例 中为 NewsArticleWeibo 类型实现了 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。我们可以传递任何 NewsArticleWeibo 的实例来调用 notify。任何用其它如 Stringi32 的类型调用该函数的代码都不能编译,因为它们没有实现 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) {

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

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

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

6.通过 + 指定多个 trait bound

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

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
where
    T: 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,但是返回了 NewsArticleWeibo 就行不通:

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

这里尝试返回 NewsArticleWeibo。但不能编译,因为 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 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C#二开业务冠邑】通过界面查看数据来源

前言 重构框架&#xff08;CS【C#】转BS【Java】&#xff09;时&#xff0c;突然发现公司的代码和数据库&#xff0c;有部分都没有写注释&#xff0c;嘎嘎&#xff0c;这不非常影响开发效率&#xff0c;于是乎&#xff0c;开始帮公司整理表结构和数据来源&#xff0c;也从而加…

ACREL DC energy meter Application in Indonesia

安科瑞 华楠 Abstract: This article introduces the application of Acrel DC meters in base station in Indonesia.The device is measuring current,voltage and energy together with hall current sensor. 1.Project Overview This company is located in Indonesia a…

企业省钱大法:如何通过更好的采购支出管理节省开支

在探讨如何通过更好的支出管理来增加利润之前&#xff0c;先来了解一下什么是支出管理。 支出管理是指通过收集、维护、分类和审查支出数据&#xff0c;来降低采购成本的过程。其目的还包括提高效率、监控工作流程以及保持合规性。 支出管理流程影响到整个采购周期的众多活动…

“三面一体”的业务调度方案在运营商订单运营的实践

在当前信息化时代&#xff0c;运营商的业务流程复杂度和多样性持续增长&#xff0c;多个系统、部门以及相关事务需要进行高效准确的调度。如何在这样的背景下&#xff0c;保证业务流程的顺畅&#xff0c;业务信息的实时传递以及业务决策的准确性&#xff0c;是业务运营面临的重…

KT142C语音芯片搭配HAA2018功放,两个板子,一个声音正常一个没有声音

KT142C芯片搭配HAA2018功放&#xff0c;焊了两个板子&#xff0c;一个声音正常&#xff0c;一个没有声音&#xff0c;该如何分析解决问题呢&#xff1f; 客户提出来的这个问题&#xff0c;原理图如下&#xff1a;分析问题的步骤如下&#xff1a; 可以看出来芯片的原理图部分没有…

深度了解LinkedBlockingQueue底层实现原理

文章目录 前言一、Queue接口的定义二、AbstractQueue实现Queue的基本操作1.AbstractQueue源码注释解析2.方法add、remove、element、clear、addAll的实现原理 三、BlockingQueue接口定义解析1.入列操作2.出列操作3.其他操作 四、LinkedBlockingQueue源码解析1.LinkedBlockingQu…

基于JAVA+SpringBoot+VUE+微信小程序的前后端分离咖啡小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着社会的快速发展和…

报错!Jupyter notebook 500 : Internal Server Error

Jupyter notebook 报错 500 : Internal Server Error 问题背景 tensorflow-gpu环境&#xff0c;为跑特定代码专门开了一个环境&#xff0c;使用conda安装了Jupyter notebook&#xff0c;能够在浏览器打开Jupyter notebook&#xff0c;但是notebook打开ipynb会报错。 问题分析…

基于单片机的公共场所马桶设计(论文+源码)

1.系统设计 本课题为公共场所的马桶设计&#xff0c;其整个系统架构如图2.1所示&#xff0c;其采用STC89C52单片机为核心控制器&#xff0c;结合HC-SR04人体检测模块&#xff0c;压力传感器&#xff0c;LCD1602液晶&#xff0c;蜂鸣器&#xff0c;L298驱动电路等构成整个系统&…

7-tcp 三次握手和四次挥手、osi七层协议,哪七层,每层有哪些?tcp和udp的区别?udp用在哪里了?

1 tcp 三次握手和四次挥手 2 osi七层协议&#xff0c;哪七层&#xff0c;每层有哪些 3 tcp和udp的区别&#xff1f;udp用在哪里了&#xff1f; 1 tcp 三次握手和四次挥手 # tcp协议---》处于osi7层协议的传输层&#xff0c;可靠连接&#xff0c;使用三次握手&#xff0c;四次挥…

一文详解!SRM(供应商管理)助力实现采购端实现降本增效

供应商管理关系到企业各部门的正常运转&#xff0c;一个好的SRM供应商管理系统对于公司来说无疑是锦上添花&#xff0c;改善企业与供应商的关系&#xff0c;可以帮助企业实现采购端的降本增效。但在信息化转型的浪潮下&#xff0c;很多企业SRM信息化却遇到不少问题。 那么请花…

媒体格式转换软件Permute 3 mac中文版软件特点

Permute mac是一款媒体格式转换软件&#xff0c;可以帮助用户快速地将各种音频、视频和图像文件转换成所需格式&#xff0c;并提供了一些常用工具以便于用户进行编辑和处理。 Permute mac软件特点 - 支持大量格式&#xff1a;支持几乎所有常见的音频、视频和图像格式&#xff…

redis---非关系型数据库

关系数据库与非关系型数据库 redis非关系型数据库&#xff0c;又名缓存型数据库。数据库类型&#xff1a;关系型数据库和非关系型数据库关系型数据库是一 个机构化的数据库,行和列。 列&#xff1a;声明对象。 行&#xff1a;记录对象属性。 表与表之间的的关联。 sql语句&…

leetcode算法之分治-归并

目录 1.排序数组2.数组中的逆序对3.计算右侧小于当前元素的个数4.翻转对 1.排序数组 排序数组 //分治-归并 class Solution {int tmp[50010]; public:vector<int> sortArray(vector<int>& nums) {mergeSort(nums,0,nums.size()-1);return nums;}void mergeS…

Java核心知识点整理大全7-笔记

目录 4.1.9. JAVA 锁 4.1.9.1. 乐观锁 4.1.9.2. 悲观锁 4.1.9.3. 自旋锁 4.1.9.4. Synchronized 同步锁 Synchronized 作用范围 Synchronized 核心组件 Synchronized 实现 4.1.9.5. ReentrantLock Lock 接口的主要方法 非公平锁 公平锁 ReentrantLock 与 synchronized …

[⑤ADRV902x]: TES (Transceiver Evaluation Software) 使用

前言 在ADI官网的ADRV902x系列的参考设计软件包&#xff08;地址&#xff1a;https://www.analog.com/cn/products/adrv9029.html#product-requirement &#xff09;中包含了GUI软件TES (Transceiver Evaluation Software)。软件实用的功能非常多&#xff0c;比如可以用界面的…

【C++上层应用】6. 信号 / 中断

文章目录 【 1. signal 函数 】【 2. raise函数 】 信号是由操作系统传给进程的 中断&#xff0c;会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上&#xff0c;可以通过按 CtrlC 产生中断。有些信号不能被程序捕获&#xff0c;但是下表所列信号可以在程序中捕…

山西电力市场日前价格预测【2023-11-22】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-11-22&#xff09;山西电力市场全天平均日前电价为13.55元/MWh。其中&#xff0c;最高日前电价为243.27元/MWh&#xff0c;预计出现在18:00。最低日前电价为0.00元/MWh&#xff0c;预计出现…

安卓毕业设计:基于安卓android微信小程序的便捷记账本系

运行环境 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&a…

4本期刊被踢!11月SCI/SSCI目录已更新

​2023年11月20日&#xff0c;科睿唯安更新了Web of Science核心期刊目录。 此次更新后SCIE期刊目录共包含9481本期刊&#xff0c;SSCI期刊目录共包含3551本期刊。此次SCIE & SSCI期刊目录更新&#xff0c;与上次更新&#xff08;2023年10月&#xff09;相比&#xff0c;共…