高级特性

我们首先在第 10 章的 “Traits: 定义共享行为” 中介绍了 traits,但我们没有讨论更高级的细节。现在你对 Rust 有了更多的了解,我们可以深入探讨这些细节。

关联类型

关联类型 将类型占位符与 trait 连接起来,使得 trait 方法定义可以在其签名中使用这些占位符类型。trait 的实现者将为特定实现指定要使用的具体类型,而不是占位符类型。这样,我们可以定义一个使用某些类型的 trait,而不需要在实现 trait 之前确切知道这些类型是什么。

我们在本章中描述的大多数高级特性都是很少需要的。关联类型介于两者之间:它们比本书其余部分解释的特性使用得更少,但比本章讨论的许多其他特性更常见。

一个带有关联类型的 trait 的例子是标准库提供的 Iterator trait。关联类型名为 Item,代表实现 Iterator trait 的类型正在迭代的值的类型。Iterator trait 的定义如 Listing 20-13 所示。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

类型 Item 是一个占位符,next 方法的定义显示它将返回类型为 Option<Self::Item> 的值。Iterator trait 的实现者将为 Item 指定具体类型,next 方法将返回包含该具体类型值的 Option

关联类型可能看起来与泛型类似,因为后者允许我们定义一个函数而不指定它可以处理的类型。为了检查这两个概念之间的区别,我们将查看一个名为 Counter 的类型上实现 Iterator trait 的例子,该实现指定 Item 类型为 u32

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

这种语法似乎与泛型类似。那么为什么不直接使用泛型定义 Iterator trait,如 Listing 20-14 所示呢?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

区别在于,当使用泛型时,如 Listing 20-14 所示,我们必须在每个实现中注解类型;因为我们也可以为 Counter 实现 Iterator<String> 或任何其他类型,所以我们可以为 Counter 实现多个 Iterator。换句话说,当一个 trait 有一个泛型参数时,它可以为一个类型多次实现,每次改变泛型类型参数的具体类型。当我们在 Counter 上使用 next 方法时,我们必须提供类型注解来指示我们要使用哪个 Iterator 实现。

使用关联类型时,我们不需要注解类型,因为我们不能为一个类型多次实现一个 trait。在 Listing 20-13 中使用关联类型的定义中,我们只能选择一次 Item 的类型,因为只能有一个 impl Iterator for Counter。我们不必在每次调用 Counter 上的 next 时指定我们想要一个 u32 值的迭代器。

关联类型也成为 trait 契约的一部分:trait 的实现者必须提供一个类型来代替关联类型占位符。关联类型通常有一个描述类型将如何使用的名称,并且在 API 文档中记录关联类型是一个好的做法。

默认泛型类型参数和运算符重载

当我们使用泛型类型参数时,我们可以为泛型类型指定一个默认的具体类型。这消除了 trait 实现者指定具体类型的需要,如果默认类型适用的话。你可以在声明泛型类型时使用 <PlaceholderType=ConcreteType> 语法指定默认类型。

一个非常有用的技术是 运算符重载,在这种情况下,你可以自定义运算符(如 +)在特定情况下的行为。

Rust 不允许你创建自己的运算符或重载任意运算符。但你可以通过实现与运算符相关的 traits 来重载 std::ops 中列出的操作和相应的 traits。例如,在 Listing 20-15 中,我们重载了 + 运算符以将两个 Point 实例相加。我们通过在 Point 结构体上实现 Add trait 来实现这一点。

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

add 方法将两个 Point 实例的 x 值和 y 值相加以创建一个新的 PointAdd trait 有一个名为 Output 的关联类型,它决定了 add 方法返回的类型。

此代码中的默认泛型类型在 Add trait 中。以下是它的定义:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

这段代码应该看起来很熟悉:一个带有一个方法和一个关联类型的 trait。新部分是 Rhs=Self:这种语法称为 默认类型参数Rhs 泛型类型参数(“right-hand side” 的缩写)定义了 add 方法中 rhs 参数的类型。如果我们在实现 Add trait 时不指定 Rhs 的具体类型,Rhs 的类型将默认为 Self,即我们正在实现 Add 的类型。

当我们为 Point 实现 Add 时,我们使用了 Rhs 的默认值,因为我们想要将两个 Point 实例相加。让我们看一个实现 Add trait 的例子,其中我们希望自定义 Rhs 类型而不是使用默认值。

我们有两个结构体,MillimetersMeters,它们以不同的单位保存值。这种将现有类型包装在另一个结构体中的薄包装称为 newtype 模式,我们将在 “使用 Newtype 模式在外部类型上实现外部 Traits” 部分中更详细地描述。我们希望将毫米值与米值相加,并让 Add 的实现正确地进行转换。我们可以为 Millimeters 实现 Add,并将 Meters 作为 Rhs,如 Listing 20-16 所示。

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

要将 MillimetersMeters 相加,我们指定 impl Add<Meters> 来设置 Rhs 类型参数的值,而不是使用默认的 Self

你将以两种主要方式使用默认类型参数:

  1. 扩展类型而不破坏现有代码
  2. 允许在大多数用户不需要的特定情况下进行自定义

标准库的 Add trait 是第二个目的的一个例子:通常,你会将两个相同类型的值相加,但 Add trait 提供了超越这一点的自定义能力。在 Add trait 定义中使用默认类型参数意味着大多数时候你不需要指定额外的参数。换句话说,不需要一些实现样板,使得使用 trait 更容易。

第一个目的与第二个目的类似,但方向相反:如果你想向现有 trait 添加类型参数,可以为其提供默认值,以允许扩展 trait 的功能而不破坏现有的实现代码。

消除同名方法之间的歧义

Rust 中没有任何东西阻止一个 trait 拥有与另一个 trait 的方法同名的方法,Rust 也不阻止你在一个类型上同时实现这两个 trait。还可以直接在类型上实现与 trait 方法同名的方法。

当调用同名方法时,你需要告诉 Rust 你想要使用哪一个。考虑 Listing 20-17 中的代码,我们定义了两个 trait,PilotWizard,它们都有一个名为 fly 的方法。然后我们在已经实现了 fly 方法的 Human 类型上实现了这两个 trait。每个 fly 方法都做不同的事情。

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}

当我们在 Human 实例上调用 fly 时,编译器默认调用直接在该类型上实现的方法,如 Listing 20-18 所示。

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}

运行此代码将打印 *waving arms furiously*,显示 Rust 调用了直接在 Human 上实现的 fly 方法。

要调用 Pilot trait 或 Wizard trait 的 fly 方法,我们需要使用更明确的语法来指定我们指的是哪个 fly 方法。Listing 20-19 演示了这种语法。

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

在方法名称前指定 trait 名称向 Rust 澄清了我们想要调用哪个 fly 实现。我们也可以写 Human::fly(&person),这与我们在 Listing 20-19 中使用的 person.fly() 等效,但如果不需要消除歧义,这样写会稍微长一些。

运行此代码将打印以下内容:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

因为 fly 方法有一个 self 参数,如果我们有两个 类型 都实现了一个 trait,Rust 可以根据 self 的类型推断出要使用哪个 trait 的实现。

然而,不是方法的关联函数没有 self 参数。当有多个类型或 trait 定义了具有相同函数名称的非方法函数时,Rust 并不总是知道你要指的是哪个类型,除非你使用 完全限定语法。例如,在 Listing 20-20 中,我们为一个动物收容所创建了一个 trait,该收容所希望将所有小狗命名为 Spot。我们创建了一个带有关联非方法函数 baby_nameAnimal trait。Animal trait 为结构体 Dog 实现,我们在 Dog 上也直接提供了一个关联的非方法函数 baby_name

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}

我们在 Dog 上定义的 baby_name 关联函数中实现了将所有小狗命名为 Spot 的代码。Dog 类型还实现了 Animal trait,该 trait 描述了所有动物的特征。小狗被称为 puppy,这在 Dog 上实现的 Animal trait 的 baby_name 关联函数中表达。

main 中,我们调用 Dog::baby_name 函数,该函数直接调用在 Dog 上定义的关联函数。此代码打印以下内容:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

这个输出不是我们想要的。我们想要调用我们在 Dog 上实现的 Animal trait 的 baby_name 函数,以便代码打印 A baby dog is called a puppy。我们在 Listing 20-19 中使用的指定 trait 名称的技术在这里没有帮助;如果我们将 main 改为 Listing 20-21 中的代码,我们将得到一个编译错误。

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

因为 Animal::baby_name 没有 self 参数,并且可能有其他类型实现了 Animal trait,Rust 无法确定我们要使用哪个 Animal::baby_name 实现。我们将得到以下编译器错误:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
2  |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error

为了消除歧义并告诉 Rust 我们要使用 Dog 上的 Animal 实现,而不是其他类型的 Animal 实现,我们需要使用完全限定语法。Listing 20-22 演示了如何使用完全限定语法。

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

我们在尖括号内为 Rust 提供了一个类型注解,指示我们要通过将 Dog 类型视为 Animal 来调用 Animal trait 的 baby_name 方法。此代码现在将打印我们想要的内容:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

一般来说,完全限定语法定义如下:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

对于不是方法的关联函数,不会有 receiver:只有其他参数的列表。你可以在调用函数或方法时使用完全限定语法。然而,你可以省略 Rust 可以从程序中的其他信息推断出的任何部分。只有在有多个实现使用相同名称并且 Rust 需要帮助来识别你要调用哪个实现的情况下,你才需要使用这种更详细的语法。

使用 Supertraits

有时你可能会编写一个依赖于另一个 trait 的 trait 定义:为了让一个类型实现第一个 trait,你希望要求该类型也实现第二个 trait。你会这样做,以便你的 trait 定义可以使用第二个 trait 的关联项。你的 trait 定义所依赖的 trait 称为你的 trait 的 supertrait

例如,假设我们想创建一个 OutlinePrint trait,它有一个 outline_print 方法,该方法将打印一个给定的值,格式化为用星号框起来的形式。也就是说,给定一个实现了标准库 trait DisplayPoint 结构体,结果为 (x, y),当我们调用 outline_print 时,对于一个 x1y3Point 实例,它应该打印以下内容:

**********
*        *
* (1, 3) *
*        *
**********

outline_print 方法的实现中,我们希望使用 Display trait 的功能。因此,我们需要指定 OutlinePrint trait 仅适用于也实现 Display 并提供 OutlinePrint 所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display 来做到这一点。这种技术类似于向 trait 添加 trait 绑定。Listing 20-23 展示了 OutlinePrint trait 的实现。

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}

因为我们指定了 OutlinePrint 需要 Display trait,所以我们可以使用为任何实现 Display 的类型自动实现的 to_string 函数。如果我们尝试在不添加冒号并在 trait 名称后指定 Display trait 的情况下使用 to_string,我们会得到一个错误,指出在当前作用域中没有为类型 &Self 找到名为 to_string 的方法。

让我们看看当我们尝试在一个没有实现 Display 的类型(如 Point 结构体)上实现 OutlinePrint 时会发生什么:

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

我们得到一个错误,指出需要 Display 但未实现:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4  |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors

要修复此问题,我们在 Point 上实现 Display 并满足 OutlinePrint 所需的约束,如下所示:

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

然后,在 Point 上实现 OutlinePrint trait 将成功编译,我们可以在 Point 实例上调用 outline_print 以在星号框中显示它。

使用 Newtype 模式在外部类型上实现外部 Traits

在第 10 章的 “在类型上实现 Trait” 中,我们提到了孤儿规则,该规则规定我们只能在类型上实现 trait,如果 trait 或类型,或两者都是我们 crate 本地的。可以使用 newtype 模式 绕过此限制,该模式涉及在元组结构体中创建一个新类型。(我们在第 5 章的 “使用没有命名字段的元组结构体创建不同类型” 中介绍了元组结构体。)元组结构体将有一个字段,并且是我们想要实现 trait 的类型的薄包装。然后包装类型是我们 crate 本地的,我们可以在包装类型上实现 trait。Newtype 是一个源自 Haskell 编程语言的术语。使用此模式没有运行时性能损失,并且包装类型在编译时被省略。

例如,假设我们想在 Vec<T> 上实现 Display,孤儿规则阻止我们直接这样做,因为 Display trait 和 Vec<T> 类型是在我们 crate 之外定义的。我们可以创建一个 Wrapper 结构体,它持有 Vec<T> 的实例;然后我们可以在 Wrapper 上实现 Display 并使用 Vec<T> 值,如 Listing 20-24 所示。

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}

Display 的实现使用 self.0 访问内部的 Vec<T>,因为 Wrapper 是一个元组结构体,Vec<T> 是元组中索引为 0 的项。然后我们可以在 Wrapper 上使用 Display trait 的功能。

使用此技术的缺点是 Wrapper 是一个新类型,因此它没有它所持有的值的方法。我们必须直接在 Wrapper 上实现 Vec<T> 的所有方法,以便这些方法委托给 self.0,这将允许我们像对待 Vec<T> 一样对待 Wrapper。如果我们希望新类型具有内部类型的所有方法,实现 Deref trait 以返回内部类型将是一个解决方案(我们在第 15 章的 “使用 Deref trait 将智能指针视为常规引用” 中讨论了实现 Deref trait)。如果我们不希望 Wrapper 类型具有内部类型的所有方法——例如,为了限制 Wrapper 类型的行为——我们必须手动实现我们确实想要的方法。

即使不涉及 traits,这种 newtype 模式也很有用。让我们转换焦点,看看一些与 Rust 类型系统交互的高级方法。