Rust:特征
特征:定义共享行为
特征定义了特定类型所具有的功能,并且可以与其他类型共享。我们可以使用特征以抽象方式定义共享行为。我们可以使用特征界限来指定泛型类型可以是具有特定行为的任何类型。
注意:特征与其他语言中常称为接口的功能类似,尽管也存在一些差异。
0x01. 定义特征
1 | pub trait Summary { |
0x02. 在类型上实现特征
1 | pub struct NewsArticle { |
1 | use aggregator::{Summary, Tweet}; |
0x03. 特征方法的默认实现
1 | pub trait Summary { |
1 | let article = NewsArticle { |
默认实现可以调用同一特征中的其他方法,即使这些其他方法没有默认实现。通过这种方式,特征可以提供许多有用的功能,而只需要实现者指定其中的一小部分。例如,我们可以定义 Summary 特征以具有一个需要实现的 summary_author 方法,然后定义一个 summary 方法,该方法具有调用 summary_author 方法的默认实现:
1 | pub trait Summary { |
要使用此版本的 Summary,我们只需要在类型上实现特征时定义 summary_author:
1 | impl Summary for Tweet { |
0x04. 特征作为参数
该函数在其 item 参数上调用 summary 方法,该参数是某种实现 Summary 特征的类型。
为此,我们使用 impl Trait
语法,如下所示:
1 | pub fn notify(item: &impl Summary) { |
使用任何其他类型(例如 String 或 i32)调用该函数的代码将无法编译,因为这些类型未实现 Summary。
4.1 特征界限语法
impl Trait
语法适用于简单的情况,但实际上是一种称为特征绑定的更长形式的语法糖;它看起来像这样:
1 | pub fn notify<T: Summary>(item: &T) { |
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 | pub fn notify(item: &(impl Summary + Display)) {} |
4.3 使用 where
子句来明确特征界限
使用过多的特征边界有其缺点。每个泛型都有自己的特征边界,因此具有多个泛型类型参数的函数可以在函数名称和其参数列表之间包含大量特征边界信息,从而使函数签名难以阅读。出于这个原因,Rust 有另一种语法,用于在函数签名后的 where 子句中指定特征边界。因此,不要这样写:
1 | fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {} |
我们可以使用 where
子句,如下所示:
1 | fn some_function<T, U>(t: &T, u: &U) -> i32 |
该函数的签名不太混乱:函数名称、参数列表和返回类型靠近在一起,类似于没有大量特征边界的函数。
0x05. 返回实现特征的类型
我们还可以在返回位置使用 impl Trait
语法来返回实现特征的某种类型的值,如下所示:
1 | fn returns_summarizable() -> impl Summary { |
仅通过其实现的特征指定返回类型的能力在闭包和迭代器的上下文中特别有用。闭包和迭代器会创建只有编译器知道的类型或非常长的类型,无法指定。impl Trait 语法允许您简洁地指定函数返回某种实现 Iterator 特征的类型,而无需写出非常长的类型。
5.1 限制
但是,**只有在返回单一类型时才能使用 impl Trait
**。例如,以下代码返回 NewsArticle 或 Tweet,且返回类型指定为 impl Summary
,则无法运行:
1 | fn returns_summarizable(switch: bool) -> impl Summary { |
由于 impl Trait
语法在编译器中的实现方式存在限制,因此不允许返回 NewsArticle 或 Tweet。我们将在第 17 章的“使用允许不同类型的值的特征对象”部分介绍如何编写具有此行为的函数。
应对限制:特征对象:
特征对象
0x06. 使用特征界限来有条件地实现方法
通过使用与使用泛型类型参数的 impl
块绑定的特征,我们可以有条件地为实现指定特征的类型实现方法。
例如,示例中的类型 Pair
1 | use std::fmt::Display; |
我们还可以为任何实现了另一个特征的类型有条件地实现一个特征。满足特征界限的任何类型上的特征实现称为全面实现,在 Rust 标准库中被广泛使用。例如,标准库在任何实现了 Display 特征的类型上实现了 ToString 特征。标准库中的 impl 块类似于以下代码:
1 | impl<T: Display> ToString for T { |
因为标准库有这个统一的实现,所以我们可以在任何实现了 Display 特征的类型上调用 ToString 特征定义的 to_string 方法。例如,我们可以将整数转换为其对应的 String 值,如下所示,因为整数实现了 Display:
1 | let s = 3.to_string(); |
特征和特征界限让我们能够编写使用泛型类型参数的代码,以减少重复,同时还向编译器指定我们希望泛型具有特定行为。然后,编译器可以使用特征界限信息来检查代码中使用的所有具体类型是否提供正确的行为。在动态类型语言中,如果我们在未定义方法的类型上调用方法,则会在运行时出错。但 Rust 将这些错误移至编译时,因此我们不得不在代码运行之前修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做可以提高性能,而无需放弃泛型的灵活性。