高级特征

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

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

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

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

具有关联类型的 trait 的一个示例是 trait 中的 standard 库提供。关联的类型被命名并位于 对于值的 type ,实现 trait 的类型为 迭代。trait 的定义如 清单 所示 19-12.IteratorItemIteratorIterator

pub trait Iterator {
    type Item;

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

示例 19-12:Iterator trait 的定义 具有关联类型 Item

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

关联类型可能看起来与泛型的概念类似,因为 后者允许我们定义一个函数,而无需指定它可以是什么类型 处理。为了检查这两个概念之间的区别,我们将查看一个 在名为 trait 的类型上实现,该类型指定 类型为:IteratorCounterItemu32

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

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

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

示例 19-13:使用泛型的 Iterator trait 的假设定义

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

使用关联类型,我们不需要注释类型,因为我们不能 多次在一个类型上实现一个 trait。在示例 19-12 中,使用 定义,我们只能选择一次 的类型,因为只能有一个 。 我们不必指定我们想要一个到处都是值的迭代器 我们呼吁 .Itemimpl Iterator for Counteru32nextCounter

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

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

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

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

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

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

该方法将两个实例的值和两个实例的值相加,以创建新的 .该 trait 具有 associated type 命名,用于确定从方法返回的类型。addxPointyPointPointAddOutputadd

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

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

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

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

当我们实现 for 时,我们使用了默认的 for,因为我们 想要添加两个实例。让我们看一个实现 我们想要自定义类型的 trait,而不是使用 违约。AddPointRhsPointAddRhs

我们有两个结构体,而 ,将值保存在不同的 单位。将现有类型放在另一个结构体中的这种薄包装称为 newtype 模式,我们在“使用 newtype Pattern to Implement External traits on External Types“部分。我们想将以毫米为单位的值与以米为单位的值相加,并得到 正确执行转换的实现。我们可以将 for 实现为 ,如示例 19-15 所示。MillimetersMetersAddAddMillimetersMetersRhs

文件名: 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 trait 以将 Millimeters 添加到 Meters

要添加 和 ,我们指定将 值,而不是使用默认值 .MillimetersMetersimpl Add<Meters>RhsSelf

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

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

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

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

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

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

当调用具有相同名称的方法时,你需要告诉 Rust 你是哪一个 想要使用。考虑示例 19-16 中的代码,其中我们定义了两个 trait,和 ,它们都有一个名为 .然后,我们实施 类型上的两个特征都已具有名为 implemented 的方法 在上面。每种方法的作用都不同。PilotWizardflyHumanflyfly

文件名: 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 方法的 trait 并在 Human 类型上实现,而 fly 方法是 直接在 Human 上实现

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

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

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

要从 trait 或 trait 调用方法, 我们需要使用更明确的语法来指定我们指的是哪种方法。 示例 19-18 演示了此语法。flyPilotWizardfly

文件名: 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 阐明哪个 we want to call 的实现。我们也可以编写 ,它相当于我们使用的 在示例 19-18 中,但如果我们不需要的话,写起来会有点长 消除歧义。flyHuman::fly(&person)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*

因为该方法需要一个参数,所以如果我们有两个类型, 都实现了一个 trait,Rust 可以找出 根据 的类型使用 的 trait。flyselfself

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

我们在关联的 在 上定义的函数。该类型还实现了 trait ,它描述了所有动物都具有的特征。婴儿犬是 称为 puppies,这在与 trait 关联的函数中 trait on 的实现中表示。baby_nameDogDogAnimalAnimalDogbaby_nameAnimal

在 中,我们调用函数,该函数调用关联的 函数。此代码打印以下内容:mainDog::baby_nameDog

$ 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

这个输出不是我们想要的。我们想调用的函数 是我们实现的 trait 的一部分,因此代码会打印 。指定 trait 名称的技术 我们在示例 19-18 中使用的在这里没有帮助;如果我们将 示例 19-20,我们将得到一个编译错误。baby_nameAnimalDogA baby dog is called a puppymain

文件名: 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 不知道该 用

因为没有参数,并且可能有 其他实现 trait 的类型,Rust 无法弄清楚是哪个 实现 We Want。我们将收到这个编译器错误:Animal::baby_nameselfAnimalAnimal::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 我们想使用 for 的实现,而不是其他 for 的实现 type 时,我们需要使用完全限定的语法。示例 19-21 演示了如何 使用完全限定的语法。AnimalDogAnimal

文件名: 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 中调用 baby_name 函数为 在 Dog 上实现

我们在尖括号内为 Rust 提供了一个类型注释,该 表示我们想从 trait 中调用 method as 通过表示我们想将类型视为 for this function 调用来实现 on。这段代码现在将打印我们想要的内容:baby_nameAnimalDogDogAnimal

$ 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, ...);

对于不是方法的关联函数,不会有 : 只有其他参数的列表。您可以使用 Fully qualified 语法。但是,您可以 省略 Rust 可以从其他信息中找出的语法的任何部分 在程序中。您只需在以下情况下使用这种更详细的语法 有多个实现使用相同的名称,Rust 需要帮助 来确定要调用的实现。receiver

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

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

例如,假设我们想使用一种方法创建一个 trait,该方法将打印一个格式化的给定值,以便它是 以星号装裱。也就是说,给定一个实现 标准库 trait 生成 ,当我们调用具有 for 和 for 的实例时,它会 应打印以下内容:OutlinePrintoutline_printPointDisplay(x, y)outline_printPoint1x3y

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

在该方法的实现中,我们希望使用 trait 的功能。因此,我们需要指定 trait 仅适用于同时实现和 提供所需的功能。我们可以在 trait 定义。该技术是 类似于添加绑定到 trait 的 trait。示例 19-22 显示了一个 trait 的实现。outline_printDisplayOutlinePrintDisplayOutlinePrintOutlinePrint: DisplayOutlinePrint

文件名: 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:实现 OutlinePrint trait 需要 Display 中的功能

因为我们指定了需要 trait 的 trait,所以我们 可以使用为任何类型的自动实现的函数 实现 .如果我们尝试在不添加 冒号并在 trait name 后指定 trait,我们将得到一个 错误地指出没有找到 in 中的类型的 method named 当前范围。OutlinePrintDisplayto_stringDisplayto_stringDisplayto_string&Self

让我们看看当我们尝试在 没有实现,例如 struct:OutlinePrintDisplayPoint

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

我们收到一个错误,指出 that is required but not implemented: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

为了解决这个问题,我们实现 on 并满足所需的 constraint,如下所示:DisplayPointOutlinePrint

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

然后实现 trait on 将编译 成功,我们可以调用一个实例来显示 它在星号的轮廓内。OutlinePrintPointoutline_printPoint

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

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

例如,假设我们想在 上实现 ,其中 孤儿规则阻止我们直接执行作,因为 trait 和 type 是在 crate 外部定义的。我们可以创建一个 struct 它保存 ;然后我们就可以实现 on 并使用这个值,如示例 19-23 所示。DisplayVec<T>DisplayVec<T>WrapperVec<T>DisplayWrapperVec<T>

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

使用 implements 访问内部 , 因为 是一个元组结构,并且是 元。然后,我们可以在 上使用 trait 的功能。Displayself.0Vec<T>WrapperVec<T>DisplayWrapper

使用这种技术的缺点是它是一种新型,因此它 没有它所持有的价值的方法。我们必须实施 的所有方法都直接对 on 使得 delegate 传递给 ,这将允许我们完全像 .如果我们希望新类型具有内部类型所具有的所有方法, 实现 trait(在“Treat Smart”的第 15 章中讨论 指针,如具有 Deref 特征的常规引用“部分)返回 内部类型将是一个解决方案。如果我们不希望类型具有 inner 类型的所有方法,例如,限制类型的 行为 — 我们只需要手动实现我们想要的方法。WrapperVec<T>Wrapperself.0WrapperVec<T>DerefWrapperWrapperWrapper

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

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