Rust:特征

特征:定义共享行为

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

注意:特征与其他语言中常称为接口的功能类似,尽管也存在一些差异。

0x01. 定义特征

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

0x02. 在类型上实现特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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());
}

0x03. 特征方法的默认实现

1
2
3
4
5
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
1
2
3
4
5
6
7
8
9
10
11
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());

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

1
2
3
4
5
6
7
pub trait Summary {
fn summarize_author(&self) -> String;

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

要使用此版本的 Summary,我们只需要在类型上实现特征时定义 summary_author:

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

0x04. 特征作为参数

该函数在其 item 参数上调用 summary 方法,该参数是某种实现 Summary 特征的类型。
为此,我们使用 impl Trait 语法,如下所示:

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

使用任何其他类型(例如 String 或 i32)调用该函数的代码将无法编译,因为这些类型未实现 Summary。

4.1 特征界限语法

impl Trait 语法适用于简单的情况,但实际上是一种称为特征绑定的更长形式的语法糖;它看起来像这样:

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

impl Trait 语法很方便,在简单情况下可以使代码更简洁,而更完整的特征绑定语法可以在其他情况下表达更多的复杂性。例如,我们可以有两个实现 Summary 的参数。使用 impl Trait 语法执行此操作如下所示:

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

如果我们希望此函数允许 item1 和 item2 具有不同的类型(只要两种类型都实现 Summary),则使用 impl Trait 是合适的。但是,如果我们想强制两个参数具有相同的类型,则必须使用特征绑定,如下所示:

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

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

4.2 使用 + 语法指定多个特征界限

我们还可以指定多个特征绑定。假设我们希望通知使用显示格式以及对项目进行汇总:我们在通知定义中指定项目必须同时实现显示和汇总。我们可以使用 + 语法来实现:

1
2
3
4
5
pub fn notify(item: &(impl Summary + Display)) {}

// `+` 语法对于泛型类型的特征界限也有效:

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

4.3 使用 where 子句来明确特征界限

使用过多的特征边界有其缺点。每个泛型都有自己的特征边界,因此具有多个泛型类型参数的函数可以在函数名称和其参数列表之间包含大量特征边界信息,从而使函数签名难以阅读。出于这个原因,Rust 有另一种语法,用于在函数签名后的 where 子句中指定特征边界。因此,不要这样写:

1
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

我们可以使用 where 子句,如下所示:

1
2
3
4
5
6
7
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// pass
}

该函数的签名不太混乱:函数名称、参数列表和返回类型靠近在一起,类似于没有大量特征边界的函数。

0x05. 返回实现特征的类型

我们还可以在返回位置使用 impl Trait 语法来返回实现特征的某种类型的值,如下所示:

1
2
3
4
5
6
7
8
9
10
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 Trait 语法允许您简洁地指定函数返回某种实现 Iterator 特征的类型,而无需写出非常长的类型。

5.1 限制

但是,**只有在返回单一类型时才能使用 impl Trait**。例如,以下代码返回 NewsArticle 或 Tweet,且返回类型指定为 impl Summary,则无法运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 语法在编译器中的实现方式存在限制,因此不允许返回 NewsArticle 或 Tweet。我们将在第 17 章的“使用允许不同类型的值的特征对象”部分介绍如何编写具有此行为的函数。

应对限制:特征对象:
特征对象

0x06. 使用特征界限来有条件地实现方法

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

例如,示例中的类型 Pair 始终实现 new 函数以返回 Pair 的新实例(回想一下第五章的“定义方法”部分,Self 是 impl 块类型的类型别名,在本例中为 Pair)。但在下一个 impl 块中,如果 Pair 的内部类型 T 实现了启用比较的 PartialOrd 特征和启用打印的 Display 特征,则 Pair 仅实现 cmp_display 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 }
}
}

// 为 <T: Display + PartialOrd> 特征界限的类型 T 实现 Pair 方法
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);
}
}
}

我们还可以为任何实现了另一个特征的类型有条件地实现一个特征。满足特征界限的任何类型上的特征实现称为全面实现,在 Rust 标准库中被广泛使用。例如,标准库在任何实现了 Display 特征的类型上实现了 ToString 特征。标准库中的 impl 块类似于以下代码:

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

因为标准库有这个统一的实现,所以我们可以在任何实现了 Display 特征的类型上调用 ToString 特征定义的 to_string 方法。例如,我们可以将整数转换为其对应的 String 值,如下所示,因为整数实现了 Display:

1
let s = 3.to_string();

特征和特征界限让我们能够编写使用泛型类型参数的代码,以减少重复,同时还向编译器指定我们希望泛型具有特定行为。然后,编译器可以使用特征界限信息来检查代码中使用的所有具体类型是否提供正确的行为。在动态类型语言中,如果我们在未定义方法的类型上调用方法,则会在运行时出错。但 Rust 将这些错误移至编译时,因此我们不得不在代码运行之前修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做可以提高性能,而无需放弃泛型的灵活性。