高级特性

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

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

关联类型将类型占位符与特征(trait)关联起来,使得特征方法定义可以在它们的签名中使用这些占位符类型。特征的实现者将指定在特定实现中使用的具体类型,以替代占位符类型。这样,我们可以在不知道这些类型具体是什么的情况下定义一个使用某些类型的特征,直到特征被实现。

我们已经将本章中的大多数高级功能描述为很少需要的。关联类型处于中间位置:它们的使用频率低于书中其他部分解释的功能,但高于本章讨论的许多其他功能。

一个带有关联类型的特征的例子是标准库提供的 Iterator 特征。关联类型名为 Item,代表了实现 Iterator 特征的类型所迭代的值的类型。 Iterator 特征的定义如清单 20-12 所示。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-12: The definition of the Iterator trait that has an associated type Item

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

关联类型可能看起来与泛型类似,因为后者允许我们在不指定可以处理的类型的情况下定义函数。为了考察这两个概念之间的区别,我们将查看一个在名为 Counter 的类型上实现 Iterator 特性的示例,该示例指定了 Item 类型为 u32

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

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listing A hypothetical definition of the `Iterator` trait using generics

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

使用关联类型时,我们不需要标注类型,因为我们不能对同一类型多次实现同一特征。在使用关联类型的定义中(如清单 20-12 所示),我们只能选择一次 Item 的类型,因为只能有一个 impl Iterator for Counter。我们不必在每次调用 Counternext 方法时都指定我们想要一个 u32 值的迭代器。

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

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

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

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

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

Filename: 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 }
    );
}
Listing 20-14: Implementing the Add trait to overload the + operator for Point instances

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 上为 Meters 作为 Rhs 实现 Add,如清单 20-15 所示。

Filename: 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))
    }
}
Listing 20-15: Implementing the Add trait on Millimeters to add Millimeters to Meters

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

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

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

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

第一个目的与第二个类似但相反:如果您想为现有特质添加一个类型参数,可以为其提供一个默认值,以便在不破坏现有实现代码的情况下扩展特质的功能。

用于消除歧义的完全限定语法:调用同名方法

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

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

Filename: 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() {}
Listing 20-16: Two traits are defined to have a method and are implemented on theHumantype, and aflymethod is implemented onHuman` directly

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

Filename: 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();
}
Listing 20-17: Calling fly on an instance of Human

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

要从 Pilot 特性或 Wizard 特性中调用 fly 方法, 我们需要使用更明确的语法来指定我们指的是哪个 fly 方法。 列表 20-18 演示了这种语法。

Filename: 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();
}
Listing 20-18: Specifying which trait’s fly method we want to call

在方法名前指定特征名可以向 Rust 澄清我们想要调用哪个 fly 的实现。我们也可以写 Human::fly(&person),这与我们在清单 20-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 并不总是知道你指的是哪个类型。例如,在清单 20-19 中,我们为一个动物收容所创建了一个特征,该收容所希望将所有小狗命名为 Spot。我们创建了一个 Animal 特征,其中包含一个关联的非方法函数 baby_name。该 Animal 特征被实现为结构体 Dog,我们还在其上直接提供了一个关联的非方法函数 baby_name

Filename: 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());
}
Listing 20-19: A trait with an associated function and a type with an associated function of the same name that also implements the trait

我们在 Dog 上定义的 baby_name 关联函数中实现了将所有小狗命名为 Spot 的代码。Dog 类型还实现了描述所有动物特征的 Animal 特性。小狗被称为幼犬,这一点在 Dog 上实现的 Animal 特性的 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特征一部分的baby_name函数,使代码打印A baby dog is called a puppy。我们在列表20-18中使用的指定特征名称的技术在这里没有帮助;如果我们把main改为列表20-20中的代码,我们将得到一个编译错误。

Filename: 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());
}
Listing 20-20: Attempting to call the baby_name function from the Animal trait, but Rust doesn’t know which implementation to use

因为 Animal::baby_name 没有 self 参数,并且可能有其他类型实现了 Animal 特性,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 我们想要使用 DogAnimal 实现而不是其他类型的 Animal 实现,我们需要使用完全限定语法。列表 20-21 展示了如何使用完全限定语法。

Filename: 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());
}
Listing 20-21: Using fully qualified syntax to specify that we want to call the baby_name function from the Animal trait as implemented on Dog

我们正在为 Rust 提供一个类型注解,位于尖括号内,这表示我们希望调用 Animal 特性在 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 需要帮助来确定您想要调用哪个实现的情况下,才需要使用这种更冗长的语法。

使用超特质来要求一个特质的功能在另一个特质中

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

例如,假设我们想要创建一个 OutlinePrint 特性,其中包含一个 outline_print 方法,该方法将以星号框住的形式打印给定的值。也就是说,给定一个实现了标准库特性 DisplayPoint 结构体,其输出为 (x, y),当我们对一个 x1y3Point 实例调用 outline_print 时,它应该打印以下内容:

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

在实现 outline_print 方法时,我们希望使用 Display 特性(trait)的功能。因此,我们需要指定 OutlinePrint 特性将仅适用于也实现了 Display 并提供 OutlinePrint 需要的功能的类型。我们可以在特性定义中通过指定 OutlinePrint: Display 来实现这一点。这种技术类似于为特性添加特性约束。列表 20-22 显示了 OutlinePrint 特性的一个实现。

Filename: 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() {}
Listing 20-22: Implementing the OutlinePrint trait that requires the functionality from Display

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

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

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

Filename: 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 特性将成功编译,我们可以调用 outline_printPoint 实例上显示它,用星号轮廓包围。

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

在第 10 章的 “在类型上实现特征” 部分,我们提到了孤儿规则,该规则指出我们只有在特征或类型之一属于我们的 crate 时,才允许在类型上实现特征。可以使用 newtype 模式 来绕过此限制,这涉及在元组结构体中创建一个新类型。(我们在第 5 章的 “使用无命名字段的元组结构体创建不同类型” 部分介绍了元组结构体。)元组结构体将有一个字段,并且是我们想要实现特征的类型的薄包装器。然后,包装器类型属于我们的 crate,我们可以在包装器上实现特征。Newtype 这个术语源自 Haskell 编程语言。使用此模式不会产生运行时性能损失,并且编译时会省略包装器类型。

作为示例,假设我们想要在 Vec<T> 上实现 Display,但由于孤儿规则,我们不能直接这样做,因为 Display 特性和 Vec<T> 类型都是在我们的 crate 之外定义的。我们可以创建一个包含 Vec<T> 实例的 Wrapper 结构体;然后我们可以在 Wrapper 上实现 Display 并使用 Vec<T> 值,如清单 20-23 所示。

Filename: 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}");
}
Listing 20-23: Creating a Wrapper type around Vec<String> to implement Display

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

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

这种新类型模式即使在不涉及特征的情况下也非常有用。让我们
转换焦点,看看一些与Rust类型系统交互的高级方法。