Rust之泛型、特性和生命期(三):Traits:定义共同的行为

news2024/11/23 19:21:32

开发环境

  • Windows 10
  • Rust 1.70.0

 

  •  VS Code 1.79.2

项目工程

这里继续沿用上次工程rust-demo

Traits:定义共同的行为

Trait定义了一个特定类型所具有的功能,并且可以与其他类型共享。我们可以使用特质以抽象的方式来定义共享行为。我们可以使用特质的界限来指定一个通用类型可以是任何具有某些行为的类型。 

注意:traits类似于其他语言中通常称为接口的特性,尽管有一些不同之处。

定义Trait 

一个类型的行为包括我们可以在该类型上调用的方法。如果我们可以在所有这些类型上调用相同的方法,那么不同的类型就有相同的行为。特质定义是一种将方法签名组合在一起的方式,以定义一组完成某些目的所需的行为。 

例如,假设我们有多个结构来保存各种类型和数量的文本:一个NewsArticle结构,保存在特定地点的新闻报道;一个Tweet结构,最多可以有280个字符,还有元数据,表明它是一个新的Tweet,一个转发,或对另一个Tweet的回复。

我们想做一个名为aggregator的媒体聚合库,可以显示可能存储在NewsArticleTweet实例中的数据摘要。要做到这一点,我们需要每种类型的摘要,我们将通过调用实例上的summaryize方法来请求该摘要。示例12显示了一个表达这种行为的公共Summary属性的定义。

文件名:src/lib.rs 

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

示例12:一个由summaryize方法所提供的行为组成的Summary Trait

在这里,我们使用 trait 关键字声明一个 trait,然后是 trait 的名字,在这个例子中是 Summary。我们还将该特性声明为pub,这样依赖于该板条箱的板条箱也可以使用该特性,我们将在几个例子中看到。在大括号内,我们声明了描述实现该特性的类型的行为的方法签名,在本例中是 fn summarize(&self) -> String。 

在方法签名之后,我们没有在大括号内提供一个实现,而是使用了一个分号。每个实现trait的类型都必须为该方法的主体提供自己的自定义行为。编译器会强制要求任何具有Summary trait的类型都要用这个签名定义summaryize方法。

一个trait在其主体中可以有多个方法:方法签名每行列出一个,每行以分号结尾。 

在一个类型上实现一个Trait

现在我们已经定义了Summary trait方法的所需签名,我们可以在我们的媒体聚合器中的类型上实现它。示例13 显示了在 NewsArticle 结构上对 Summary 特质的实现,它使用标题、作者和位置来创建 summaryize 的返回值。对于Tweet结构,我们将summaryize定义为用户名和整个tweet的文本,假设tweet内容已经被限制在280个字符以内。

 文件名:src/lib.rs

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

示例13:在NewsArticleTweet类型上实现Summary Trait

在一个类型上实现特质与实现常规方法类似。不同的是,在 impl 之后,我们要写上我们想要实现的特质名称,然后使用 for 关键字,再指定我们想要实现特质的类型名称。在 impl 块中,我们把特质定义中的方法签名放进去。我们没有在每个签名后面添加分号,而是使用大括号,并在方法体中填写我们希望特质的方法对特定类型的具体行为。

Tweet在库已经在NewsArticleTweet上实现了Summary特性,crate的用户可以在NewsArticle和的实例上调用特性方法,就像我们调用常规方法一样。唯一的区别是,用户必须把特质和类型都带入范围。下面是一个二进制板条箱如何使用我们的聚合器库板条箱的例子: 

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

 这段代码打印了"1 new tweet: horse_ebooks: of course, as you probably already know, people."

依赖于aggregator板块的其他板块也可以将 Summary 特质带入范围,在自己的类型上实现 Summary。需要注意的一个限制是,只有当特质或类型中至少有一个是我们板条箱的本地类型时,我们才能在一个类型上实现特质。例如,我们可以在自定义类型(如Tweet)上实现标准库特性(如Display),作为我们聚合器板块功能的一部分,因为Tweet这个类型是我们聚合器板块的本地。我们也可以在我们的聚合器箱中实现Vec<T>Summary,因为Summary这个特性是我们aggregator箱的本地特性。

但是我们不能在外部类型上实现外部特性。例如,我们不能在我们的aggregator箱中对 Vec<T> 实现 Display 特质,因为 Display Vec<T> 都是在标准库中定义的,并不是我们aggregator箱的本地类型。这个限制是一个叫做一致性的属性的一部分,更确切地说,是孤儿规则,因为父类型不存在而被命名。这个规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有这个规则,两个板块可以为同一类型实现相同的特性,而Rust不知道该使用哪个实现。

默认实现

有时,为特质中的某些或所有方法设置默认行为,而不是要求对每个类型的所有方法进行实现,是非常有用的。然后,当我们在一个特定类型上实现特质时,我们可以保留或覆盖每个方法的默认行为。

在示例14中,我们为Summary trait的summaryize方法指定了一个默认字符串,而不是像示例12中那样只定义方法签名。

 文件名:src/lib.rs

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

示例14: 定义一个带summaryize方法的默认实现的Summary特质 

为了使用默认的实现来总结NewsArticle的实例,指定一个空的代码块:impl Summary for NewsArticle {}。

 尽管我们不再直接定义NewsArticlesummaryize方法,但我们已经提供了一个默认的实现,并指定NewsArticle实现了Summary特性。因此,我们仍然可以在NewsArticle的一个实例上调用summaryize方法,就像这样:

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

这段代码打印:New article available! (Read more...)

创建一个缺省实现并不要求我们改变示例13中Summary on Tweet的实现。原因是覆盖默认实现的语法与实现没有默认实现的特质方法的语法相同。

默认实现可以调用同一特质中的其他方法,即使这些其他方法没有默认实现。通过这种方式,一个trait可以提供很多有用的功能,而只需要实现者指定其中的一小部分。例如,我们可以定义Summary trait,使其有一个需要实现的summaryize_author方法,然后定义一个summaryize方法,该方法有一个调用summaryize_author方法的默认实现:

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

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

要使用这个版本的Summary,我们只需要在类型上实现该特性时定义summaryize_author

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

 在我们定义了summaryize_author之后,我们可以在Tweet结构的实例上调用summaryize,而summaryize的默认实现将调用我们提供的summaryize_author的定义。因为我们已经实现了summaryize_authorSummary trait已经给了我们summaryize方法的行为,而不需要我们再写任何代码。

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

这段代码打印:1 new tweet: (Read more from @horse_ebooks...)

注意,不可能从同一方法的重写实现中调用默认实现。

作为参数的Traits

现在你知道了如何定义和实现Trait,我们可以探索如何使用特质来定义接受许多不同类型的函数。我们将在示例13中使用我们在NewsArticleTweet类型上实现的Summary trait来定义一个通知函数,该函数在其item参数上调用summaryize方法,该参数是一些实现了Summary trait的类型。要做到这一点,我们使用 impl Trait 语法,像这样:

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

 我们为item参数指定 impl 关键字和trait名称,而不是具体的类型。这个参数接受任何实现了指定trait的类型。在 notify 的主体中,我们可以调用 item上来自 Summary 特质的任何方法,比如 summaryize。我们可以调用notify 并传入NewsArticleTweet的任何实例。用任何其他类型(如Stringi32)调用该函数的代码将不会被编译,因为这些类型没有实现Summary

Trait Bound句法

impl Trait语法适用于简单的情况,但实际上是一种较长形式的语法糖,被称为trait bound;它看起来像这样: 

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

这种较长的形式等同于上一节的例子,但更加啰嗦。我们将特质边界与通用类型参数的声明放在冒号之后和角括号之内。

impl Trait语法很方便,在简单的情况下可以使代码更简洁,而在其他情况下,更完整的trait bound语法可以表达更多的复杂性。例如,我们可以有两个参数来实现Summary。用 impl Trait 语法这样做,看起来像这样:

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

如果我们想让这个函数允许 item1 item2 有不同的类型(只要两个类型都实现了 Summary),那么使用 impl Trait是合适的。然而,如果我们想强制两个参数具有相同的类型,我们必须使用trait bound,像这样:

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

被指定为item1item2参数类型的通用类型T约束了该函数,使得作为item1item2参数传递的值的具体类型必须是相同的。

用 "+"语法指定多个Traits Bound

我们还可以指定一个以上的trait bound。假设我们想让notifyitem上使用显示格式以及summary:我们在notify的定义中指定item必须同时实现Displaysummary。我们可以使用 "+"语法来这样做:

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

+ 语法在通用类型的trait bound上也是有效的:

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

有了这两个trait bound的指定,notify的主体可以调用summaryize并使用{}来格式化项目。

带有where子句的更清晰的Traits 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,
{

 这个函数的签名不那么杂乱:函数名、参数列表和返回类型紧挨着,类似于没有大量特质界限的函数。

返回实现Traits的类型

我们也可以在返回位置使用 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,
    }
}

通过使用 impl Summary 作为返回类型,我们指定 returns_summarizable 函数返回一些实现 Summary 特质的类型,而不需要命名具体类型。在这种情况下,returns_summarizable返回一个Tweet,但调用此函数的代码不需要知道这一点。

在闭包和迭代器的上下文中,只通过它所实现的特征来指定返回类型的能力特别有用,我们在后续章中介绍了这一点。闭包和迭代器创建了只有编译器知道的类型,或者指定了非常长的类型。impl Trait语法可以让你简洁地指定一个函数返回某个实现了Iterator trait的类型,而不需要写出一个很长的类型。

然而,你只能在返回单一类型的时候使用 impl Trait。例如,这段返回NewsArticleTweet的代码,其返回类型被指定为implum Summary,则无法使用:

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

由于编译器对impl Trait语法实现的限制,返回NewsArticleTweet是不允许的。

使用Traits Bound有条件地实现方法

 通过使用trait与使用通用类型参数的impl块绑定,我们可以为实现指定trait的类型有条件地实现方法。例如,示例15 中的类型 Pair<T> 总是实现 new 函数,以返回Pair<T> 的新实例(记得在前面章的 "定义方法 "一节中,Self 是植入块的类型的别名,在这里是 Pair<T> )。但是在下一个impl块中,Pair<T> 只有在其内部类型 T 实现了能够进行比较的 PartialOrd 特质和能够进行打印的 Display 特质的情况下才会实现 cmp_display 方法。

文件名:src/lib.rs

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

示例15: 根据trait bound,有条件地在通用类型上实现方法

我们也可以对任何实现了另一个trait 的类型有条件地实现一个trait。在任何满足trait bound的类型上实现trait 被称为空白实现,在Rust标准库中被广泛使用。例如,标准库在任何实现了Display特质的类型上实现了ToString特质。标准库中的植入块看起来类似于这段代码: 

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

 因为标准库有这种很多的实现,我们可以在任何实现了Display特质的类型上调用ToString特质所定义的to_string方法。例如,我们可以像这样把整数变成它们相应的字符串值,因为整数实现了Display

let s = 3.to_string();

空白的实现出现在trait的文档中的 "实施者 "部分。

trait和trait bound让我们在写代码时可以使用通用类型的参数来减少重复,同时也可以向编译器说明我们希望通用类型具有特定的行为。然后,编译器可以使用特质约束信息来检查我们代码中使用的所有具体类型是否提供了正确的行为。在动态类型语言中,如果我们在一个没有定义方法的类型上调用一个方法,我们会在运行时得到一个错误。但是Rust将这些错误转移到了编译时,所以我们不得不在代码运行之前就修复这些问题。此外,我们不必在运行时编写检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。

本章重点

  • Trait概念
  • 定义Trait
  • 基于具体类型实现Trait
  • Trait的默认实现
  • Trait作为参数的传递方法
  • 实现了Trait的返回类型

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

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

相关文章

低学历又如何?我这样的程序员照样可以逆袭

今天分享的这个主题&#xff0c;很可能会带来争议&#xff0c;因为目前优秀毕业生0年就可以拿到 20K 的待遇&#xff0c;这里暂且抛开硕士&#xff0c;985&#xff0c;211的 Top 前几高学校本科生。 毕竟今天的主题的初衷是地点低的程序员如何才能 2-3 年实现 20K 的目的&…

mysql根据一个表的数据更新另一个表数据的SQL写法

目录 问题描述解决办法&#xff08;推荐第三种&#xff09; 问题描述 概述&#xff1a;用一个表中的字段去更新另外一个表中的字段&#xff0c; MySQL 中有相应的 update 语句来支持&#xff0c;不过这个 update 语法有些特殊。看一个例子就明白了。 解决办法&#xff08;推…

机器学习笔记 - 基于MobileNetV2的迁移学习训练关键点检测器

一、下载数据集 StanfordExtra数据集包含12000张狗的图像以及关键点和分割图图。 GitHub - benjiebob/StanfordExtra:12k标记的野外狗实例,带有2D关键点和分割。我们的 ECCV 2020 论文发布的数据集:谁把狗排除在外?3D 动物重建,循环中期望最大化。https://github.com/benj…

驱动模块和printk函数

目录 1. 驱动模板 1.1. 在源码工程路径下创建.c文件 1.2. 编写驱动模板 1.3. 将模板放到ubuntu上 1.4. 书写Makefile 1.5. 编译和安装 2. printk 2.1. Source Insight查找命令 2.2. printk讲解 2.2.1. 分析函数 2.2.2. 编写代码 2.3. 拓展 2.3.1. 关于printk函数测…

《人工智能.一种现代方法》原版精读思维导读-第一章

目录 书籍 前言部分 封面故事 完整目录 1. Intruduction简介 Whats AI The Foundations of AI The History of AI The State of the Art Risks and Benefits of AI Summary 2. Intelligent Agents 书籍 人工智能.一种现代方法 Artificial Intelligence. The Moder…

ArcGis系列-坐标系转换

Arcgis的工程项目可以添加各种类型的空间资源&#xff0c;比如数据库空间表、shp文件&#xff0c;每张空间表的坐标系可能都会有差异&#xff0c;把他们放到一个工程里时可以统一设置坐标系。 本文将介绍ArcGis三个需要坐标转换的场景&#xff1a; Arcgis Pro设置项目坐标GP分…

WPy64的Python开发环境中安装pinyin库方法举例和应用

WPy64的Python开发环境中安装拼音库&#xff08;pypinyin&#xff09;方法举例和应用 在Python开发环境中安装拼音库后&#xff0c;我们就可以实现对汉字的注音显示。下面以WPy64为例子&#xff0c;讲解pypinyin库的安装方法。 步骤&#xff1a; 一、找到WPy64所安装的目录中…

Vue中如何进行分布式事务管理与分布式事务解决方案

Vue中如何进行分布式事务管理与分布式事务解决方案 在分布式系统中&#xff0c;事务管理是一个非常重要的问题。如果没有良好的事务管理&#xff0c;分布式系统可能会导致数据不一致的问题。本文将介绍Vue中如何进行分布式事务管理以及分布式事务解决方案。 什么是分布式事务&…

二叉搜索树(Binary Search Tree)的模拟实现

前言 为什么要学习二叉搜索树呢&#xff1f;因为set 和 map的底层实际上就是一颗二叉搜索树&#xff0c;只不过是被进行了一些特殊的处理&#xff0c;所有了解二叉搜索树的底层实现有利于我们更好的理解的map和set的原理。二叉搜索树又叫二叉排序树&#xff0c;它或者是一颗空树…

数据库系统概述——第二章 关系数据库(知识点复习+练习题)

&#x1f31f;博主&#xff1a;命运之光 &#x1f984;专栏&#xff1a;离散数学考前复习&#xff08;知识点题&#xff09; &#x1f353;专栏&#xff1a;概率论期末速成&#xff08;一套卷&#xff09; &#x1f433;专栏&#xff1a;数字电路考前复习 &#x1f99a;专栏&am…

SpringBoots利用redis实现防止接口幂等性重复提交

目录 什么是幂等性&#xff1f; 应用场景分析 解决办法 实际使用 什么是幂等性&#xff1f; 接口的幂等性就是用户对于同一个操作发起的一次请求或者多次请求的结果都是一致的&#xff0c;不会因为多次点击而产生副作用&#xff0c;比如说经典的支付场景&#xff1a;用户购…

一款超级给力的弱网测试神器—Qnet(上)

一、APP弱网测试背景 App在使用的过程中&#xff0c;难免会遇到不同的弱网络环境&#xff0c;像在公车上、在地铁、地下车库等。在这种情况下&#xff0c;手机常常会出现网络抖动、上行或下行超时&#xff0c;导致APP应用中出现丢包延迟&#xff0c;从而影响用户体验。 作为软…

推荐10款测试员常用的单元测试工具

前言 随着DevOp的不断流行&#xff0c;自动化测试慢慢成为Java开发者的关注点。因此&#xff0c;本文将分享10款优秀的单元测试框架和库&#xff0c;它们可以帮助Java开发人员在其Java项目上编写单元测试和集成测试。 1. JUnit 我绝对JUnit不需要太多的介绍了。即使您是Java…

Spring Security OAuth2.0认证授权 --- 高级篇

六、OAuth2.0 6.1、OAuth2.0介绍 OAuth&#xff08;开放授权&#xff09;是一个开放标准&#xff0c;允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息&#xff0c;而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续…

基于Python接口自动化测试框架(初级篇)附源码

目录 引言 框架设计思路 框架结构 运行程序 总结 总结&#xff1a; 引言 很多人都知道&#xff0c;目前市场上很多自动化测试工具&#xff0c;比如&#xff1a;Jmeter&#xff0c;Postman&#xff0c;TestLink等&#xff0c;还有一些自动化测试平台&#xff0c;那为啥还要…

【Unity Shader】从入门到感慨万千(2)用C#画一个立方体

文章目录 一、构成一个立方需要多少个顶点?二、定义三角面的索引数组:三、定义UV坐标数组:四、最后构建Mesh:五、完整代码:一、构成一个立方需要多少个顶点? 这个问题是面试经常被问到的题。如上图,我们知道在几何中立方体有6个面,8个顶点。但在图形学中,顶点指的是模…

vulhub-struts2-S2-007 远程代码执行漏洞复现

漏洞描述 影响版本: 2.0.0 - 2.2.3 原理 当配置了验证规则 <ActionName>-validation.xml 时&#xff0c;若类型验证转换出错&#xff0c;后端默认会将用户提交的表单值通过字符串拼接&#xff0c;然后执行一次 OGNL 表达式解析并返回。例如这里有一个 UserAction&…

Vue中如何进行表单手机号验证与手机号归属地查询

Vue中如何进行表单手机号验证与手机号归属地查询 在Vue中&#xff0c;表单验证和数据处理是非常重要的功能&#xff0c;它可以帮助我们保证用户输入的数据的正确性和完整性。手机号验证和手机号归属地查询是常见的表单验证需求&#xff0c;本文将介绍如何在Vue中实现这两个功能…

13.推荐系统

例如一个电影推荐系统&#xff0c;一共有n个用户&#xff0c;m个电影&#xff0c;每部电影都有一定的特征&#xff0c;例如爱情片的比例、动作片的比例。n个用户对看过的电影进行评分&#xff0c;推荐系统如何给用户推荐新电影&#xff0c;预测用户对新电影的评分&#xff1f; …

三、IK分词器

目录 1、IK分词器下载 2、下载完毕后解压&#xff0c;放入到elasticsearch的plugins下即可 3、重启elasticsearch&#xff0c;可以看到ik分词器被加载了 4、也可以通过elasticsearch-plugin这个命令来查看加载进来的插件 5、使用kibana测试ik分词器 6、扩展配置ik分词器词典…