Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

19.2 高级 trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype

19.2.1 在 trait 定义中使用关联类型来指定占位类型

我们首先在 10.3. trait Pt.1:trait的定义、约束与实现 介绍了 trait,但没有讨论更高级的细节。现在来深入了解。

关联类型是 trait 内部的类型占位符。它可以用于 trait 方法签名中。它用来为某些类型定义 trait,而无需事先知道这些类型是什么。

例如:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

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

标准库的 Iterator trait 就是一个带有关联类型的 trait,其定义如上所示。

Item 就是关联类型。在迭代过程中,用 Item 代替实际值的类型,从而把逻辑和具体数据类型分开。你可以在 next 的返回类型 Option<Self::Item> 中看到 Item

Item 是一个类型占位符。它的核心思想与泛型类似,但也有区别:

泛型关联类型
每次实现 trait 时都要指定类型无需指定类型
同一个类型可以用不同的泛型参数多次实现同一个 trait同一个类型不能多次实现同一个 trait

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

使用泛型参数时,我们可以给泛型一个默认的具体类型。语法是 <PlaceholderType=ConcreteType>。这项技术常用于运算符重载。

虽然 Rust 不允许你创建自己的运算符,也不能重载任意运算符,但你可以通过实现 std::ops 中列出的那些 trait 来重载某些运算符。

看一个例子:

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 }
    );
}
  • 在这个例子中,我们为 Point 结构体实现了 Add trait,从而重载了 + 运算符。具体来说,Add 中的 add 函数逐字段相加。
  • main 中,我们可以直接用 + 把两个 Point 值相加。

Add trait 的定义如下:

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

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

它使用了默认泛型参数 Rhs=Self。这意味着当我们实现 Add 时,如果不给 Rhs 指定具体类型,默认类型就是 Self。所以上面例子中的 RhsPoint

现在再看另一个例子,这次是毫米和米相加:

#![allow(unused)]
fn main() {
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))
    }
}
}
  • 这里把 MillimetersMeters 声明为元组结构体,分别表示毫米和米。
  • 我们为 Millimeters 实现 Add,并显式指定另一个类型是 Meters。在 add 中,我们把存储的毫米值与换算成毫米后的米值相加。

19.2.3 默认泛型参数的主要用例

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

19.2.4 使用完全限定语法调用同名方法

直接看例子:

#![allow(unused)]
fn main() {
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*");
    }
}
}
  • 我们定义了两个 trait:PilotWizard,各自都有一个 fly 方法,但没有具体实现。
  • 我们有一个 Human 结构体。接下来分别为它实现两个 trait,也就是为每个 trait 提供一个 fly 方法。此外,我们还在结构体自己的 impl 块中实现了一个 fly 方法。

此时一共有三个 fly 方法。如果在 main 中这样调用:

fn main() {
    let person = Human;
    person.fly();
}

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

要调用 Pilot trait 或 Wizard trait 中的 fly,需要用更明确的语法指出我们指的是哪一个 fly

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

在方法名前指定 trait 名,可以告诉 Rust 我们想要哪一个 fly 实现。person.fly() 也可以写成 Human::fly(&person)

输出:

This is your captain speaking.
Up!
*waving arms furiously*

然而,不是方法的关联函数没有 self 参数。当来自不同类型或 trait 的多个方法或关联函数同名时,除非使用完全限定语法,Rust 并不总是知道你指的是哪一个:

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());
}
  • Animal trait 有一个 baby_name 函数。Dog 是一个结构体,实现了 Animal trait,同时也在自己的 impl 块中实现了 baby_name。所以现在有两个 baby_name 函数。
  • main 中使用了 Dog::baby_name(),因此按上面的逻辑,会运行 Dog 自己的 impl 块中的 baby_name 实现,得到 Spot

输出:

A baby dog is called a Spot

那么如何调用 DogAnimal trait 的 baby_name 实现呢?我们试试上一个例子的逻辑:

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

输出:

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

Animal trait 上的 baby_name 函数需要知道使用哪个类型的实现,但 baby_name 本身没有参数,所以 Rust 无法推断指的是哪个类型的实现。

这时就需要完全限定语法。它的形式是:

#![allow(unused)]
fn main() {
<Type as Trait>::function(receiver_if_method, next_arg, ...);
}

这种语法可以在任何调用函数或方法的地方使用,并且可以忽略那些能从其它上下文推断出来的部分。

但只有在 Rust 无法区分你想要哪个具体实现时,才需要这种语法,因为它写起来很麻烦。所以一般来说,除非必要,否则不要使用它。

按这个语法,上面的代码应改为:

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

输出:

A baby dog is called a puppy

19.2.5 使用 supertrait 要求额外的 trait 功能

有时我们需要在一个 trait 中使用另一个 trait 的功能,这意味着那个被间接要求的 trait 也必须被实现。那个被间接要求的 trait 就是当前 trait 的 supertrait。

例如:

#![allow(unused)]
fn main() {
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));
    }
}
}

OutlinePrint 实际上用来在终端用字符打印一个形状。但在打印时,self 必须实现 to_string,这意味着 self 必须实现 Display trait(to_string 来自 ToString trait,而任何实现了 Display 的类型都会自动实现 ToString)。写法是 trait 关键字 + trait 名 + : + supertrait。

假设我们有一个 Point 结构体,想用 OutlinePrintoutline_print 方法在终端打印它。因为 OutlinePrint 要求 Display,我们必须同时实现 OutlinePrintDisplay,否则会失败:

#![allow(unused)]
fn main() {
struct Point {
    x: i32,
    y: i32,
}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

impl OutlinePrint for Point {}
}

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

我们已经讨论过孤儿规则:只有当 trait 或类型定义在本地 crate 中时,才能为该类型实现这个 trait。我们可以使用 newtype 模式绕过这条规则,具体做法是用元组结构体在本地构建一个新类型。

例如:

假设我们想为 Vec<String> 实现 Display,但 VecDisplay 都定义在我们的 crate 之外,所以不能直接为 Vec<String> 实现。于是我们把这个向量包进自己的元组结构体 Wrapper,再为 Wrapper 实现 Display

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