高级特征

我们首先在“特征:定义共享 行为“部分 10 中,但我们没有讨论更高级的细节。现在您知道更多 关于 Rust,我们可以深入了解细节。

使用关联类型在 Trait Definitions 中指定占位符类型

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

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

具有关联类型的 trait 的一个示例是Iteratortrait 的 standard 库提供。关联的类型名为Item并替身 对于值的类型,实现Iteratortrait 是 迭代。的定义Iteratortrait 如 清单 所示 19-12.

pub trait Iterator {
    type Item;

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

示例 19-12:Iterator特性 具有关联类型Item

类型Item是一个占位符,而nextmethod 的定义显示 它将返回Option<Self::Item>.的Iteratortrait 将指定Itemnext方法将返回一个Option包含该具体类型的值。

关联类型可能看起来与泛型的概念类似,因为 后者允许我们定义一个函数,而无需指定它可以是什么类型 处理。为了检查这两个概念之间的区别,我们将查看一个 实现Iteratortrait 的Counter指定 这Itemtype 为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
        }
    }
}

此语法似乎与泛型的语法相当。那么为什么不直接定义Iteratortrait 替换为泛型,如示例 19-13 所示?

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

示例 19-13:Iterator使用泛型的 trait

区别在于,当使用泛型时,如示例 19-13 所示,我们必须 注释每个 implementation中的类型;因为我们也可以实现Iterator<String> for Counter或任何其他类型,我们可以有多个 的实现IteratorCounter.换句话说,当特征具有 generic 参数,它可以多次为一个类型实现,更改 每次泛型类型参数的具体类型。当我们使用nextmethod 开启Counter,我们必须为 指示Iterator我们想使用。

使用关联类型,我们不需要注释类型,因为我们不能 多次在一个类型上实现一个 trait。在示例 19-12 中,使用 定义,我们只能选择Item将是一次,因为只能有一个impl Iterator for Counter. 我们不必指定我们想要一个u32无处不在的价值观 我们称之为nextCounter.

关联类型也成为 trait 契约的一部分:的 trait 必须提供一个 type 来代替关联的 type placeholder。 关联类型通常具有描述如何使用类型的名称。 在 API 文档中记录关联的类型是一种很好的做法。

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

当我们使用泛型类型参数时,我们可以为 泛型类型。这消除了 trait 的实现者对 如果默认类型有效,请指定具体类型。指定默认类型 当使用<PlaceholderType=ConcreteType>语法。

此技术有用的一个很好的示例是使用运算符 重载,其中自定义运算符(如 ) 在特定情况下。+

Rust 不允许你创建自己的运算符或重载任意 运营商。但是,您可以重载列出的作和相应的特征 在std::ops通过实施与 Operator 关联的特征。为 例如,在示例 19-14 中,我们重载了 operator 以添加两个+Point实例一起。我们通过实现AddtraitPoint结构:

文件名: 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:实现Addtrait 重载 的运算符+Point实例

add方法添加x值为 2Point实例和y值为 2Point实例以创建新的Point.这Addtrait 具有 关联的类型Output,它决定了从add方法。

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

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

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

这段代码应该看起来大致很熟悉:一个具有一个方法的 trait 和一个 associated 类型。新部分是Rhs=Self:此语法称为 default 类型参数。这Rhs泛型类型参数(“right hand”的缩写 side“) 定义rhs参数中的add方法。如果我们不这样做 指定具体类型Rhs当我们实现Addtrait 时,类型 之Rhs将默认为Self,这将是我们正在实现的类型Add上。

当我们实施AddPoint,我们使用了Rhs因为我们 想加两个Point实例。让我们看一个实现 这Addtrait 中,我们想要自定义Rhs键入,而不是使用 违约。

我们有两个结构体,MillimetersMeters,将值保存在不同的 单位。将现有类型放在另一个结构体中的这种薄包装称为 newtype 模式,我们在“使用 newtype Pattern to Implement External traits on External Types“部分。我们想将以毫米为单位的值与以米为单位的值相加,并得到 实施Add正确进行转换。我们可以实施AddMillimetersMeters作为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:实现Addtrait 开启Millimeters添加MillimetersMeters

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

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

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

标准库的Addtrait 是第二个目的的一个例子: 通常,您将添加两个 like 类型,但Addtrait 提供了 除此之外进行定制。在Add特性 定义意味着您不必指定大多数 时间。换句话说,不需要一些实现样板,使 使用 trait 更容易。

第一个目的与第二个目的类似,但方向相反:如果要添加 type 参数添加到现有 trait 中,您可以为其指定默认值以允许 在不破坏现有 implementation code 的 implementation code 中。

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

Rust 中没有任何内容可以阻止 trait 具有与 another trait 的方法,Rust 也不会阻止你实现这两个 trait 在一种类型上。也可以使用 与 traits 中的方法同名。

当调用具有相同名称的方法时,你需要告诉 Rust 你是哪一个 想要使用。考虑示例 19-16 中的代码,我们定义了两个 trait,PilotWizard,这两个方法都有一个名为fly.然后,我们实施 类型上的两个 traitHuman已经有一个名为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:定义了两个 trait 以具有fly方法,并在Humantype 和flymethod 为 实施日期Human径直

当我们调用flyHuman,编译器默认调用 直接在类型上实现的方法,如示例 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:调用flyHuman

运行此代码将打印*waving arms furiously*,显示 Rust 称为fly方法实现于Human径直。

要调用fly方法中的Pilottrait 或Wizard特性 我们需要使用更明确的语法来指定哪个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:指定哪个 trait 的fly方法 we 想要调用

在方法名称之前指定 trait name 会向 Rust 阐明哪个 实现fly我们想打电话。我们也可以编写Human::fly(&person),它相当于person.fly()我们使用的 在示例 19-18 中,但如果我们不需要的话,写起来会有点长 消除歧义。

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

$ 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*

因为flymethod 采用selfparameter 参数,如果我们有两个类型, 都实现了一个 trait,Rust 可以找出 trait 来根据self.

但是,不是方法的关联函数没有self参数。当有多个类型或特征定义非方法 函数具有相同的函数名称,Rust 并不总是知道你是哪种类型 表示,除非你使用完全限定的语法。例如,在示例 19-19 中,我们 为想要将所有婴儿狗命名为 Spot 的动物收容所创建一个特征。 我们制作一个Animaltrait 替换为关联的非方法函数baby_name. 这Animaltrait 为 struct 实现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:具有关联函数和 type 替换为同名的关联函数,该函数还实现 特性

我们在baby_name相关 在Dog.这Dogtype 也实现了 traitAnimal,它描述了所有动物都具有的特征。婴儿犬是 称为 puppies,这体现在实现Animaltrait 开启Dogbaby_name函数与Animal特性。

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

这个输出不是我们想要的。我们想调用baby_name函数 是Animaltrait 实现的Dog所以代码打印出来A baby dog is called a puppy.指定 trait 名称的技术 我们在示例 19-18 中使用的在这里没有帮助;如果我们更改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:尝试调用baby_name函数Animaltrait 的 intent 中,但 Rust 不知道该用哪个 用

因为Animal::baby_name没有self参数,并且可能存在 实现Animaltrait 中,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 我们想使用AnimalDogAnimal对于其他 type 时,我们需要使用完全限定的语法。示例 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:使用完全限定语法指定 我们想要调用baby_name函数Animaltrait 设置为 实施日期Dog

我们在尖括号内为 Rust 提供了一个类型注释,该 表示我们想要调用baby_name方法从Animaltrait 设置为 实施日期Dog通过表示我们想要处理Dogtype 作为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: 只有其他参数的列表。您可以使用 Fully qualified 语法。但是,您可以 省略 Rust 可以从其他信息中找出的语法的任何部分 在程序中。您只需在以下情况下使用这种更详细的语法 有多个实现使用相同的名称,Rust 需要帮助 来确定要调用的实现。

使用 supertrait 要求一个特征在另一个特征中的功能

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

例如,假设我们想创建一个OutlinePrinttrait 中具有outline_print方法,该方法将打印一个格式化的给定值,以便它是 以星号装裱。也就是说,给定一个Point结构体实现 标准库特征Display以产生(x, y),当我们调用outline_printPoint实例,该实例具有1x3y它 应打印以下内容:

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

outline_print方法,我们希望使用Displaytrait 的功能。因此,我们需要指定OutlinePrinttrait 仅适用于同时实现Display和 提供的功能OutlinePrint需要。我们可以在 trait 定义OutlinePrint: Display.该技术是 类似于添加绑定到 trait 的 trait。示例 19-22 显示了一个 实现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));
    }
}

fn main() {}

示例 19-22:实现OutlinePrinttrait 需要Display

因为我们已经指定了OutlinePrint需要Displaytrait 的 可以使用to_string为任何类型的自动实现的函数 实现Display.如果我们尝试使用to_string而不添加 colon 并指定Displaytrait 的 trait 中,我们会得到一个 错误地表示没有名为to_string找到&Self在 当前范围。

让我们看看当我们尝试实现OutlinePrint在类型上 不实现Display,例如Point结构:

文件名: 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

为了解决这个问题,我们实施了DisplayPoint并满足OutlinePrintrequires,如下所示:

文件名: 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();
}

然后实现OutlinePrinttrait 开启Point将编译 成功,我们可以调用outline_printPoint要显示的实例 它在星号的轮廓内。

使用 newtype 模式在外部类型上实现 external trait

在第 10 章的 “在 Type“部分,我们提到了 orphan 规则,它规定我们只允许在类型上实现 trait,如果 trait 或 type 都是我们 crate 的本地。有可能获得 使用 newType 模式绕过此限制,这涉及创建一个 new 类型。(我们在“使用 Tuple 没有命名字段的结构体来创建不同类型的“部分。元组结构将有一个字段,并且是一个 thin 包装器。然后,包装器 type 是我们的 crate 的本地类型,我们可以在 wrapper 上实现 trait。Newtype 是一个源自 Haskell 编程语言的术语。 使用此模式不会对运行时性能造成影响,并且包装器 type 在编译时被省略。

例如,假设我们想要实现DisplayVec<T>,其中 孤立规则阻止我们直接执行作,因为Displaytrait 和Vec<T>type 在我们的 crate 之外定义。我们可以制作一个Wrapper结构 它包含一个Vec<T>;然后我们就可以实施DisplayWrapper并使用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:创建一个Wrapper键入周围Vec<String>实施Display

的实现Display使用self.0访问内部Vec<T>, 因为Wrapper是一个元组结构,Vec<T>是 元。然后我们可以使用Displaytrait 开启Wrapper.

使用这种技术的缺点是Wrapper是一个新类型,因此它 没有它所持有的价值的方法。我们必须实施 的所有方法Vec<T>直接打开Wrapper这样,方法 delegate toself.0,这将允许我们将WrapperVec<T>.如果我们希望新类型具有内部类型所具有的所有方法, 实现Dereftrait 中(在第 15 章的“治疗 Smart 指针(如常规引用)中带有Deref性状”部分)在Wrapper返回 内部类型将是一个解决方案。如果我们不想让Wrappertype to have inner 类型的所有方法,例如,要限制Wrappertype 的 行为 — 我们只需要手动实现我们想要的方法。

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准