高级特征


我们首先在“特征:定义共享 Behavior“部分,但我们没有讨论更高级的细节。现在你对 Rust 有了更多的了解,我们可以深入了解细节。


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


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


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


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

pub trait Iterator {
    type Item;

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


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


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


关联类型似乎与泛型的概念相似,因为泛型允许我们定义一个函数,而无需指定它可以处理哪些类型。为了检查这两个概念之间的区别,我们将查看 Iterator trait 在名为 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 trait 替换为泛型,如示例 19-13 所示?

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


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


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


使用关联类型,我们不需要注释类型,因为我们不能 多次在一个类型上实现一个 trait。在示例 19-12 中,使用 定义,我们只能选择 Item 将是一次,因为 Counter 只能有一个 impl Iterator。我们不必指定我们想要一个 u32 值的迭代器,我们在 Counter 上调用 next


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


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


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


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


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


文件名: 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 实例的 + 运算符


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:此语法称为 default 类型参数Rhs 泛型类型参数(“right hand side”的缩写)定义 add 方法中 rhs 参数的类型。如果我们在实现 Add trait 时没有为 Rhs 指定具体类型,则 Rhs 的类型将默认为 Self,这将是我们正在实现的类型 附加


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


我们有两个结构体,MillimetersMeters,它们保存不同 单位。将现有类型在另一个结构体中的这种薄包装称为 newtype 模式,我们将在“使用 Newtype Pattern to Implement External traits on External Types“部分。我们希望将以毫米为单位的值与以米为单位的值相加,并让 Add 的实现正确地进行转换。我们可以实现 AddMeters 作为 RhsMillimeters,如示例 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 (毫米) 将 Millimeters (毫米) 添加到 Meters (米


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


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


  • 在不破坏现有代码的情况下扩展类型

  • 为了允许在特定情况下进行自定义,大多数用户不需要


标准库的 Add trait 是第二个目的的一个示例:通常,您将添加两个 like 类型,但 Add trait 提供了在此范围之外进行自定义的功能。在 Add trait 定义中使用默认类型参数意味着大多数情况下您不必指定额外的参数。换句话说,不需要一些 implementation 样板,从而更容易使用 trait。


第一个目的与第二个目的类似,但相反:如果要向现有 trait 添加 type 参数,则可以为其提供默认值,以允许在不破坏现有实现代码的情况下扩展 trait 的功能。


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


Rust 中没有任何内容可以阻止一个 trait 具有与另一个 trait 的方法同名的方法,Rust 也不会阻止你在一种类型上实现这两个 trait。也可以直接在类型上实现一个方法,其名称与 trait 中的方法相同。


当调用具有相同名称的方法时,你需要告诉 Rust 你是哪一个 想要使用。考虑示例 19-16 中的代码,我们定义了两个 trait, PilotWizard,它们都有一个叫做 fly 的方法。然后,我们在 Human 类型上实现这两个 trait,该类型已经实现了一个名为 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 方法并在 Human 类型上实现,而 fly 方法直接在 Human 上实现


当我们在 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:在


运行这段代码会打印出 *挥舞手臂 farriously*,表明 Rust 直接调用了在 Human 上实现的 fly 方法。


要从 Pilot trait 或 Wizard trait 调用 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:指定要调用的 trait 的 fly 方法


在方法名称之前指定 trait name 可以向 Rust 阐明我们要调用哪个 fly 实现。我们也可以编写 Human::fly(&person) 的 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 参数,如果我们有两个类型都实现一个 trait,Rust 可以根据 self 的类型来确定要使用哪个 trait 实现。


但是,不是方法的关联函数没有 self 参数。当有多个类型或特征定义非方法 函数具有相同的函数名称,Rust 并不总是知道你是哪种类型 表示,除非你使用完全限定的语法。例如,在示例 19-19 中,我们为一个动物收容所创建了一个 trait,该动物收容所希望将所有幼犬命名为 Spot。我们使用关联的非方法函数 baby_name 创建一个 Animal trait。Animal trait 是为结构体 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:具有关联函数的 trait 和具有同名关联函数的类型,该函数也实现了该 trait


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


这个输出不是我们想要的。我们想调用 baby_name 函数,它是我们在 Dog 上实现的 Animal trait 的一部分,因此代码打印 婴儿狗被称为小狗。我们在示例 19-18 中使用的指定 trait name 的技术在这里没有帮助;如果我们将 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 function 的 Animal trait 中,但 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 我们想使用 Animal for Dog 与其他类型的 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:使用完全限定语法指定我们想从 Dog 上实现的 Animal trait 调用 baby_name 函数


我们在尖括号内为 Rust 提供了一个类型注释,这表明我们想从 Dog 上实现的 Animal trait 调用 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, ...);


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


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


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


例如,假设我们想让 OutlinePrint 特征具有 outline_print 一种方法,它将打印一个给定的值,其格式使其以星号为框架。也就是说,给定一个 Point 结构体,该结构体实现了标准库 trait Display 以产生 (x, y),当我们调用 outline_print 在 1 表示 x3 表示 yPoint 实例上,它应打印以下内容:

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


outline_print 方法的实现中,我们希望使用 显示特征的功能。因此,我们需要指定 OutlinePrint trait 仅适用于同时实现 Display 并提供 OutlinePrint 所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display 来做到这一点。此技术类似于添加绑定到 trait 的 trait。示例 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,所以我们可以使用 to_string 函数,该函数会自动实现任何实现 Display 的类型。如果我们尝试使用 to_string 而不添加冒号并在 trait 名称后指定 Display trait,我们将收到一个错误,指出在当前范围内没有找到类型 &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


为了解决这个问题,我们实现了 Display on Point 并满足 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 特征将成功编译,我们可以在 Point 实例上调用 outline_print 以在星号轮廓中显示它。


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


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


例如,假设我们想在 Vec<T> 上实现 Display ,孤立规则阻止我们直接执行此作,因为 Display 特征和 Vec<T> 类型在我们的 crate 之外定义。我们可以创建一个 Wrapper 结构体,其中包含 Vec<T> 的实例;那么我们就可以在 Wrapper 并使用 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> 实现显示


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


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


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