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

这篇具有很好参考价值的文章主要介绍了Rust之泛型、特性和生命期(三):Traits:定义共同的行为。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

开发环境

  • Windows 10
  • Rust 1.70.0

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

  •  VS Code 1.79.2

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

项目工程

这里继续沿用上次工程rust-demo
Rust之泛型、特性和生命期(三):Traits:定义共同的行为

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将这些错误转移到了编译时,所以我们不得不在代码运行之前就修复这些问题。此外,我们不必在运行时编写检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。文章来源地址https://www.toymoban.com/news/detail-492798.html

本章重点

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

到了这里,关于Rust之泛型、特性和生命期(三):Traits:定义共同的行为的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Go 泛型之泛型约束

    目录 Go 泛型之泛型约束 一、引入 二、最宽松的约束:any 三、支持比较操作的内置约束:comparable 四、自定义约束 五、类型集合(type set) 六、简化版的约束形式 七、约束的类型推断 八、小结 虽然泛型是开发人员表达“通用代码”的一种重要方式,但这并不意味着所有泛型

    2024年02月04日
    浏览(40)
  • C++之泛型算法

    大多数算法在头文件algorithm中,在头文件numeric中定义了一组数值泛型算法。理解算法最基本的方法就是了解他们是否读取元素,改变元素,或是重排元素顺序 使用STL算法经常需要使用函数对象和函数适配器,所以需要#include 所有算法都被设计用来处理一个或多个iterator区间,

    2024年02月03日
    浏览(35)
  • Java之泛型

    泛型: 泛型实现了参数化类型的概念,使代码可以应用于多种类型。 泛型的目的是希望类或方法能够具有最广泛的表达能力。 Java的泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。 泛型类: 类型参数:用尖括号括住,实际

    2024年02月11日
    浏览(37)
  • TypeScript 进阶之泛型

    避免代码重复和创建可重用类型是编写干净代码的重要部分。 将所有类型属性都设置为可选 Required 与 Partial 相反。它构造一个类型,其中需要该类型的所有属性。它可用于确保没有可选属性出现在类型中。 多属性的对象中摘取某些属性。 键可以是字符串文字或字符串文字的

    2024年01月23日
    浏览(41)
  • java入坑之泛型

    Java泛型是JDK 5中引入的一个新特性,它提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这意味着你可以使用一套代码来处理多种不同类型的数据 ArrayListE,E表示元素El

    2024年02月10日
    浏览(38)
  • 用可视化案例讲Rust编程4. 用泛型和特性实现自适配shapefile的读取

    本节已经涉及Rust学习曲线上的一个大坑:泛型和特性了,属于语言的深水区,如果初学者,建议看一眼知道有这个功能即可。 如果我们立足于功能实现,那么做到像上一节那样就可以了,从原理上来说,每个函数满足唯一的功能,是一种好的设计,软件工程里面“高内聚低

    2024年01月25日
    浏览(40)
  • 选择排序算法之泛型优化

    工作原理: 每一次从待排序的数据元素中选中最小的一个元素,然后,再从剩余未排序元素中继续寻找最小元素,将2个元素交换位置,就达到了已排序的元素一直是从小到大了。 这个算法的时间复杂度为O(n²),空间复杂度为O(1)。 当然,为了匹配多种类型的对象,可以使用

    2024年02月06日
    浏览(45)
  • C#(六十二)之泛型的约束

    类型约束 基类约束有两个重要的目的。 1:它允许在泛型类中使用有约束指定的基类成员。 2:确保只能使用支持指定基类或派生类的类型实例。 约束是使用 where 上下文指定的。 下表列出了五种类型的约束: 约束 说明 T:struct 类型参数必须是值类型。可以指定除

    2024年02月17日
    浏览(43)
  • 【Rust 基础篇】Rust 特征(Traits)

    Rust 的特征(Traits)是一种强大的语言机制,它允许我们定义共享行为并实现代码的抽象。通过特征,我们可以编写更加灵活和可复用的代码。本篇博客将详细介绍 Rust 特征的定义、实现和使用方法,包括特征的基本语法、默认实现、泛型特征以及特征的实现和使用。 在 Ru

    2024年02月15日
    浏览(50)
  • 从零开始学习 Java:简单易懂的入门指南之泛型及set集合(二十二)

    1.1泛型概述 泛型的介绍 ​ 泛型是JDK5中引入的特性,它提供了编译时类型安全检测机制 泛型的好处 把运行时期的问题提前到了编译期间 避免了强制类型转换 泛型的定义格式 类型: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如: 类型1,类型2…: 指定多

    2024年02月09日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包