高级特性

我们首先在第 10 章的 “特性:定义共享行为”部分介绍了特性,但我们没有讨论更高级的细节。现在你对 Rust 了解得更多了,我们可以深入研究细节了。

使用关联类型在特性定义中指定占位符类型

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

我们已经将本章中的大多数高级特性描述为很少需要。关联类型介于两者之间:它们的使用频率低于本书其余部分中解释的特性,但高于本章中讨论的许多其他特性。

具有关联类型的特性的一个示例是标准库提供的 Iterator 特性。关联类型名为 Item,代表实现 Iterator 特性的类型正在迭代的值的类型。Iterator 特性的定义如列表 19-12 所示。

pub trait Iterator {
    type Item;

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

列表 19-12:具有关联类型 ItemIterator 特性的定义

类型 Item 是一个占位符,next 方法的定义表明它将返回 Option<Self::Item> 类型的值。Iterator 特性的实现者将指定 Item 的具体类型,并且 next 方法将返回包含该具体类型值的 Option

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

文件名:src/lib.rs

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 特性,如列表 19-13 所示?

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

列表 19-13:使用泛型的 Iterator 特性的假设定义

区别在于,当使用泛型时,如列表 19-13 中所示,我们必须在每个实现中注释类型;因为我们还可以为 Counter 实现 Iterator<String> 或任何其他类型,所以我们可以为 Counter 实现多个 Iterator 实现。换句话说,当一个特性具有泛型参数时,可以为一个类型多次实现它,每次都更改泛型类型参数的具体类型。当我们在 Counter 上使用 next 方法时,我们必须提供类型注释来指示我们要使用哪个 Iterator 实现。

使用关联类型,我们不需要注释类型,因为我们不能多次对一个类型实现特性。在列表 19-12 中使用关联类型的定义,我们只能选择 Item 的类型一次,因为只能有一个 impl Iterator for Counter。我们不必在每次调用 Counter 上的 next 时都指定我们需要 u32 值的迭代器。

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

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

当我们使用泛型类型参数时,我们可以为泛型类型指定默认的具体类型。这消除了特性实现者在默认类型起作用时指定具体类型的需要。当使用 <PlaceholderType=ConcreteType> 语法声明泛型类型时,指定默认类型。

一个很好的例子是运算符重载,其中你可以自定义特定情况下运算符(例如 +)的行为。

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

文件名:src/main.rs

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

列表 19-14:实现 Add 特性以重载 Point 实例的 + 运算符

add 方法将两个 Point 实例的 x 值和两个 Point 实例的 y 值相加,以创建一个新的 PointAdd 特性具有一个名为 Output 的关联类型,该类型确定 add 方法返回的类型。

此代码中的默认泛型类型在 Add 特性中。这是它的定义

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

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

此代码应看起来非常熟悉:一个具有一个方法和一个关联类型的特性。新部分是 Rhs=Self:此语法称为默认类型参数Rhs 泛型类型参数(“右侧”的缩写)定义 add 方法中 rhs 参数的类型。如果我们在实现 Add 特性时未指定 Rhs 的具体类型,则 Rhs 的类型将默认为 Self,这将是我们正在实现 Add 的类型。

当我们为 Point 实现 Add 时,我们使用了 Rhs 的默认值,因为我们想添加两个 Point 实例。让我们看一个实现 Add 特性的示例,其中我们想自定义 Rhs 类型,而不是使用默认值。

我们有两个结构 MillimetersMeters,它们以不同的单位保存值。这种将现有类型薄薄地包装在另一个结构中的做法称为新类型模式,我们在 “使用新类型模式在外部类型上实现外部特性”部分中对此进行了更详细的描述。我们想将毫米值添加到米值,并让 Add 的实现正确进行转换。我们可以为 Millimeters 实现 Add,其中 Meters 作为 Rhs,如列表 19-15 所示。

文件名:src/lib.rs

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

列表 19-15:在 Millimeters 上实现 Add 特性,以将 Millimeters 添加到 Meters

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

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

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

标准库的 Add 特性是第二个目的的示例:通常,你将添加两个类似的类型,但是 Add 特性提供了除此之外进行自定义的能力。在 Add 特性定义中使用默认类型参数意味着你不必在大多数情况下指定额外的参数。换句话说,不需要一些实现样板,这使得使用特性更容易。

第一个目的类似于第二个目的,但方向相反:如果要向现有特性添加类型参数,可以为其指定一个默认值,以便在不破坏现有实现代码的情况下扩展特性的功能。

消除歧义的完全限定语法:调用具有相同名称的方法

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

在调用具有相同名称的方法时,你需要告诉 Rust 你要使用哪个方法。考虑列表 19-16 中的代码,其中我们定义了两个特性 PilotWizard,它们都具有一个名为 fly 的方法。然后,我们在类型 Human 上实现这两个特性,该类型已经在其上实现了名为 fly 的方法。每个 fly 方法的作用都不同。

文件名:src/main.rs

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() {}

列表 19-16:定义了两个具有 fly 方法的特性,并在 Human 类型上实现,并且直接在 Human 上实现 fly 方法

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

文件名:src/main.rs

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

列表 19-17:在 Human 的实例上调用 fly

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

要从 Pilot 特性或 Wizard 特性调用 fly 方法,我们需要使用更明确的语法来指定我们要调用的 fly 方法。列表 19-18 演示了此语法。

文件名:src/main.rs

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

列表 19-18:指定我们要调用的特性的 fly 方法

在方法名称前指定特性名称向 Rust 澄清了我们要调用的 fly 的哪个实现。我们也可以编写 Human::fly(&person),这与我们在列表 19-18 中使用的 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 参数,所以如果我们有两个类型都实现了一个特性,Rust 可以根据 self 的类型找出要使用哪个特性的实现。

但是,不是方法的关联函数没有 self 参数。当有多个类型或特性定义具有相同函数名称的非方法函数时,除非你使用完全限定语法,否则 Rust 并不总是知道你要使用哪个类型。例如,在列表 19-19 中,我们为想要将所有小狗命名为 Spot 的动物收容所创建一个特性。我们使用关联的非方法函数 baby_name 创建一个 Animal 特性。Animal 特性在结构体 Dog 上实现,我们还在该结构体上直接提供关联的非方法函数 baby_name

文件名:src/main.rs

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

列表 19-19:具有关联函数的特性和具有相同名称的关联函数并且还实现该特性的类型

我们实现了将所有小狗的名字都命名为 Spot 的代码,该代码位于在 Dog 上定义的关联函数 baby_name 中。Dog 类型还实现了 trait Animal,该 trait 描述了所有动物都具有的特征。幼犬被称为小狗,这在 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

这个输出不是我们想要的。我们想要调用的是作为 Animal trait 一部分实现的 baby_name 函数,这样代码会打印出 A baby dog is called a puppy。我们在列表 19-18 中使用的指定 trait 名称的技术在这里没有帮助;如果我们把 main 函数改为列表 19-20 中的代码,我们会得到一个编译错误。

文件名:src/main.rs

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

列表 19-20:尝试从 Animal trait 调用 baby_name 函数,但 Rust 不知道要使用哪个实现

由于 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 trait,而不是为其他类型实现的 Animal,我们需要使用完全限定语法。列表 19-21 展示了如何使用完全限定语法。

文件名:src/main.rs

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

列表 19-21:使用完全限定语法来指定我们想要调用 Animal trait 中为 Dog 实现的 baby_name 函数

我们在尖括号内提供了一个类型注释,它表明我们想要调用 Animal trait 中为 Dog 实现的 baby_name 方法,通过声明我们希望将 Dog 类型视为此次函数调用的 Animal。现在这段代码将打印出我们想要的内容:

$ 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 需要帮助来识别您要调用的实现时,才需要使用这种更详细的语法。

使用 Supertrait 在另一个 Trait 中要求一个 Trait 的功能

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

例如,假设我们想创建一个带有 outline_print 方法的 OutlinePrint trait,该方法将打印一个给定的值,并以星号将其框起来。也就是说,给定一个实现了标准库 trait Display 以产生 (x, y)Point 结构体,当我们在一个 x1y3Point 实例上调用 outline_print 时,它应该打印以下内容:

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

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

文件名:src/main.rs

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() {}

列表 19-22:实现需要 Display 功能的 OutlinePrint trait

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

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

文件名:src/main.rs

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 需要的约束,如下所示:

文件名:src/main.rs

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 模式在外部类型上实现外部 Trait

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

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

文件名:src/main.rs

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

列表 19-23:围绕 Vec<String> 创建一个 Wrapper 类型以实现 Display

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

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

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