Rust 学习笔记:Trait

Rust 学习笔记:Trait

trait 定义了特定类型具有的功能。

我们可以使用 trait 以抽象的方式定义共享行为。

我们可以使用 trait bounds 来指定哪些类型才是我们想要的泛型类型。

trait 类似于在其他语言中称为接口的特性,尽管存在一些差异。

定义 trait

类型的行为由可以调用该类型的方法组成。

如果我们能在不同的类型上调用相同的方法,就意味着这些不同的类型共享相同的行为。

Trait 定义:将不同的方法签名汇成一个方法签名,由此定义一套共享的行为。

假设我们有多个结构体保存不同类型和数量的文本:

  • NewsArticle 结构体:新闻报道
  • Tweet:推文

创建一个名为 aggregator 的库 crate,它可以显示可能存储在 NewsArticle 或 Tweet 实例中的数据摘要。

在这里插入图片描述

我们将通过在实例上调用 summary 方法来请求该摘要。

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

在这里,我们使用 trait 关键字和 trait 的名称来声明 trait,在本例中是 Summary。我们还将这个 trait 声明为 pub,这样依赖于这个 crate 的 crate 也可以使用这个 trait。在花括号内,我们声明了方法签名。我们没有在花括号内提供实现,而是使用分号。实现此特性的每个类型都必须为方法体提供自己的自定义行为。编译器将强制任何具有 Summary trait 的类型,其方法将使用此签名精确定义。

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

在类型上实现 trait

首先给出两个结构体的定义:

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

既然我们已经定义了 Summary trait 方法所需的签名,我们就可以实现它。

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

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

在类型上实现 trait 的过程:在 impl 之后,我们输入要实现的 trait 名,然后使用 for 关键字,然后指定要为之实现 trait 的类型名。在 impl 块中,我们放置 trait 定义所定义的方法签名,并在方法体中填充我们希望 trait 的方法针对特定类型具有的特定行为。

用户可以像调用常规方法一样调用 NewsArticle 和 Tweet 实例上的 trait 方法。唯一的区别是,用户必须将 trait 和类型一起纳入作用域。

新建一个 main.rs:

use aggregator::{Tweet, Summary};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

因为我们创建的是一个库 crate,默认执行的是 Test,只能运行一些脚本化测试。

在这里插入图片描述

现在我们想运行 main.rs 中的代码,我们可以点击 RustRover 右侧的 Cargo 按钮,点击“运行 Cargo 命令”,输入“cargo run”即可。

在这里插入图片描述

在这里插入图片描述

添加后,配置里也会多一个 run,以后用这个就行。

在这里插入图片描述

这个例子打印:1 new tweet: horse_ebooks: of course, as you probably already know, people.

其他依赖于 aggregator 的 crate 也可以将 Summary trait 引入作用域,从而在它们自己的类型上实现 Summary。

需要注意的一个限制是,只有当 trait 或类型或者两者都是本地的时候,我们才能在一个类型上实现 trait。例如,我们可以在像 Tweet 这样的自定义类型上实现像 Display 这样的标准库特性,作为我们的 aggregator crate 功能的一部分,因为 Tweet 类型是我们自定义的类型。我们也可以在 aggregator crate 中的 Vec<T> 上实现 Summary,因为 Summary trait 是我们自定义的 trait。

我们不能在外部类型上实现外部特征。例如,我们不能在 aggregator crate 中的 Vec<T> 上实现 Display trait,因为 Display 和 Vec<T> 都是在标准库中定义的。

这种限制被称为“孤儿规则”,它要求 trait 或类型必须属于当前 crate。这条规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有这个规则,两个 crate 可以为相同的类型实现相同的 trait, Rust 就不知道该使用哪个实现。

默认实现

我们可以为 trait 中的一些或所有方法设置默认行为。

当我们在特定类型上实现 trait 时,我们可以保留或覆盖每个方法的默认行为。

修改 Summary trait 的 Summary 方法,指定一个默认字符串。

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

修改代码,为 NewsArticle {} 指定一个空的 impl 块,其中包含 impl Summary。

impl Summary for NewsArticle {

}

尽管我们不再直接在 NewsArticle 上定义 Summary 方法,但我们提供了一个默认实现,并指定 NewsArticle 实现 Summary trait。因此,我们仍然可以在 NewsArticle 的实例上调用 summary 方法。

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

默认实现可以调用同一 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,只需要在对类型实现 trait 时定义 summarize_author:

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

定义 summarize_author 之后,我们可以在 Tweet 结构体的实例上调用 summarize,而 summarize 的默认实现将调用我们提供的 summarize_author 的定义。

    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new tweet: {}", tweet.summarize());

程序打印:1 new tweet: (Read more from @horse_ebooks…)

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

作为参数的 trait

使用 trait 可以定义接受许多不同类型的函数。

我们使用在 NewsArticle 和 Tweet 类型上实现的 Summary trait 来定义一个通知函数,该函数在其 item 参数上调用 Summary 方法,该参数是实现 Summary trait 的某种类型。要做到这一点,我们使用 impl Trait 语法。

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

我们没有为 item 参数指定具体类型,而是指定了 impl 关键字和 trait 名称。此参数接受实现指定 trait 的任何类型。在 notify 的主体中,我们可以对 item 调用来自 Summary trait 的任何方法。我们可以调用 notify 并传入 NewsArticle 或 Tweet 的任何实例。使用任何其他类型(如 String)调用该函数的代码将无法编译,因为这些类型不实现 Summary。

trait bound 语法

impl trait 语法适用于简单的情况,但实际上是一种更长的语法糖,称为 trait 绑定。

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

我们将 trait 约束泛型类型参数的声明放在冒号之后和尖括号内。

impl trait 语法很方便,在简单的情况下可以编写更简洁的代码,而更完整的 trait 绑定语法在其他情况下可以表达更复杂的代码。

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

如果我们希望这个函数允许 item1 和 item2 具有不同的类型(只要这两个类型都实现了 Summary),那么使用 impl trait 是合适的。但是,如果我们想强制两个形参具有相同的类型,就必须使用 trait 绑定。

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

作为 item1 和 item2 形参类型指定的泛型类型 T 约束了函数,使得作为 item1 和 item2 实参传递的值的具体类型必须相同。

用 + 语法指定多个 trait 约束

我们还可以指定多个 trait 绑定。

例如,我们在函数 notify 的定义中指定项必须同时实现 Display 和 Summary。我们可以使用 + 连接 trait。

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

+ 语法也适用于泛型类型的 trait 边界。

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

有了指定的两个 trait 边界,notify 的主体就可以调用 Summary 并使用 {} 来格式化 item。

使用 where 子句使 trait 边界更清晰

使用太多的 trait 边界使得具有多个泛型类型参数的函数可能在函数名和参数列表之间包含大量的 trait 绑定信息,让函数签名难以阅读。

由于这个原因,Rust 在函数签名后的 where 子句中指定 trait 边界。

// 这种函数签名太长了
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
👇 👇 👇
// 这样就清晰多了
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{

返回实现 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,
        repost: false,
    }
}

通过对返回类型使用 impl Summary,我们指定 returns_summarizable 函数返回实现 Summary trait 的某种类型,而不指定具体类型。在本例中,returns_summarizable 返回一个 Tweet,但是调用这个函数的代码不需要知道这个。

仅通过它实现的 trait 指定返回类型的能力在闭包和迭代器上下文中特别有用。闭包和迭代器创建的类型只有编译器知道,或者需要很长时间才能指定。impl trait 语法允许指定一个函数返回实现 Iterator trait 的某种类型,而不需要写出很长的类型。

但是,编译器只有在返回单个类型时才能使用 impl trait 语法。

例如,这段代码返回一个 NewsArticle 或一个 Tweet,返回类型指定为 impl 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,
            repost: false,
        }
    }
}

使用 trait 边界有条件地实现方法

通过使用与使用泛型类型参数的 impl 块绑定的 trait,我们可以为实现指定 trait 的类型有条件地实现方法。

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

Pair<T> 对任意的内部类型 T 都实现了 new 方法。但 Pair<T> 只有在其内部类型 T 实现了支持比较的 PartialOrd trait 和支持打印的 Display trait 时才实现 cmp_display 方法。

我们还可以为任何实现了另一个 trait 的类型有条件地实现一个 trait。

例如,标准库在任何实现 Display trait 的类型上实现 ToString trait。标准库中的 impl 块看起来类似于以下代码:

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

因为标准库有这种覆盖实现,所以我们可以在任何实现 Display trait 的类型上调用 ToString trait 定义的 to_string 方法。例如,我们可以像这样将整数转换为相应的 String 值,因为整数实现了 Display trait:

let s = 3.to_string();

trait 和 trait 边界让我们可以编写使用泛型类型参数来减少重复的代码,但也可以向编译器指定我们希望泛型类型具有特定的行为。然后,编译器可以使用 trait 绑定信息来检查代码中使用的所有具体类型是否提供了正确的行为。

在动态类型语言中,如果在没有定义方法的类型上调用方法,将在运行时得到错误。但是 Rust 将这些错误转移到编译时,因此我们被迫在代码能够运行之前修复问题。

此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值