Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10.3 trait Pt.1:trait的定义、约束与实现

10.3.1. 什么是trait

trait 的意思是特征、特质。trait 用来向 Rust 编译器描述某个类型具有哪些能力,以及它可以与其他类型共享哪些行为。trait 以抽象方式定义共享行为。

还有 trait bounds(约束)的概念,它可以把泛型类型参数约束为实现了特定行为的类型。换句话说,就是要求泛型类型参数实现某些 trait。

Rust 中的 trait 与其他语言中的接口(interface)有些类似,但仍有区别。

10.3.2. 定义一个trait

类型的行为由该类型本身可以调用的方法组成。有时不同类型具有相同的方法,这时我们就说这些类型共享相同的行为。trait 提供了一种把方法组合在一起的方式,从而定义实现某种目的所必需的行为。

  • 使用 trait 关键字定义 trait。trait 中的方法既可以只写方法签名(以 ; 结尾、没有方法体),也可以提供默认实现(后文会讲到)
  • 一个 trait 可以有多个方法,每个需要实现方提供的方法签名单独占一行,并以 ; 结尾
  • 对于没有默认实现的方法,实现该 trait 的类型必须提供具体的方法体

例如:

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
}

trait 前面加上 pub 表示它是公共的。这个 trait 名为 Summary,其中包含一个名为 summarize 的方法签名。除了 &self 之外没有其他参数,返回类型是 String,签名以 ; 结束。它没有方法体,因此没有具体实现。当然,一个 trait 下可以有很多方法签名:

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
    fn summarize1(&self) -> String;
    fn summarize2(&self) -> String;
    //......
}
}

10.3.3. 在类型上实现trait

为类型实现 trait 与为类型实现方法很类似,但也有不同之处。

为类型实现方法的写法是在 impl 关键字后面跟上类型:

#![allow(unused)]
fn main() {
impl Yyyy {....}
}

为类型实现 trait 的写法则是:

#![allow(unused)]
fn main() {
impl Xxxx for Yyyy {....}
}
  • Xxxx 指的是 trait 名
  • Yyyy 指的是类型名
  • 在花括号内,需要为 trait 中的方法签名写出具体实现

例如(lib.rs):

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}

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)
    }
}
}
  • 结构体 NewsArticle 表示新闻文章。它有四个字段:headline 表示标题,location 表示地点,author 表示作者,content 表示内容
  • 结构体 Tweet 表示 X(前身为 Twitter)上的推文。它有四个字段:usernamecontentreplyretweet

这两个结构体类型当然不同,它们的大多数字段也不同。但它们都可以有相同的行为——提供摘要 Summary,因此分别在这两个类型上实现了 Summary

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

这段代码为 NewsArticle 实现了该 trait。因为 trait 定义中包含 summarize 方法签名,所以这里必须写出具体实现:使用 format! 宏把 self.headlineself.authorself.location 组合成一个字符串并返回。

#![allow(unused)]
fn main() {
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
}

这段代码也为 Tweet 实现了该 trait,同样提供了 summarize 的具体实现:使用 format! 宏把 self.usernameself.content 组合成一个字符串并返回。

现在来到 main.rs,看看实例是如何调用的:

use RustStudy::{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());
}

记住,我们的代码写在 lib.rs 中,在 main.rs 中使用之前,需要先把它引入作用域。写法是:

#![allow(unused)]
fn main() {
use 你的package名::...::你需要的模块;
}

你的 package 名就是 Cargo.toml 中的项目名,把它复制过来即可。

引入 Summary 是因为使用了 Summary trait 下的 summarize 方法;引入 Tweet 是因为使用了 Tweet 结构体。

看一下输出:

1 new tweet: horse_ebooks: of course, as you probably already know, people

10.3.4. trait的约束

想要为某个类型实现某个 trait,前提条件是:

  • 该类型本身(例如 Tweet)或该 trait 本身(例如让 Vector 实现本地的 Summary)必须在本地 crate 中定义
  • 不能为外部类型实现外部 trait。例如,在本地 crate 中为标准库的 Vector 实现标准库的 Display trait 这个限制是语言一致性规则的一部分。更具体地说,它是孤儿规则,之所以这样命名,是因为父类型并没有定义在当前 crate 中。这条规则确保其他人的代码不能随意破坏你的代码,反之亦然。如果没有这条规则,两个 crate 可以为同一个类型实现同一个 trait,Rust 就不知道该使用哪个实现。

10.3.5. 默认实现

有时,为 trait 中的某些或全部方法提供默认行为非常有用。这让我们不必为每一个类型的实现都提供自定义行为。我们仍然可以为特定类型实现 trait 方法。

当为某些类型实现 trait 时,我们可以选择保留或覆盖每个方法的默认实现。

之前的写法是:

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
}

之前的版本只写了方法签名,没有提供实现,但实际上可以添加默认实现:

默认实现:

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
}

这里的默认实现只是返回字符串 "(Read more...)"

因为这个方法在 trait 中已经有默认实现了,所以具体类型可以直接使用该默认实现,而不必自己再提供实现。

NewsArticle 为例,它原本有自己的实现(也可以叫做对默认实现的覆盖):

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

如果删掉这个具体实现,NewsArticle 就会使用默认实现:

#![allow(unused)]
fn main() {
impl Summary for NewsArticle {}
}

还有一点需要知道:带有默认实现的方法可以调用同一 trait 中的其他方法,即使那些方法没有默认实现:

#![allow(unused)]
fn main() {
pub trait Summary {
	fn summarize_author(&self) -> String;
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
}

summarize 的默认实现调用了 summarize_author,即使 summarize_author 只是一个签名、没有具体实现。但如果想为某个类型实现 summarize,就需要先实现 summarize_author

#![allow(unused)]
fn main() {
impl Summary for NewsArticle {
	fn summarize_author(&self) -> String {
		format!("@{}", self.author)
	}
}
}

PS:由于 NewsArticle 使用了 summarize 的默认实现,所以这里不需要再写 summarize 的实现。

这种写法有一点需要注意:无法从被覆盖的方法实现中调用默认实现。