Rust 学习笔记(40)-高级trait

参考章节《Rust程序设计语言》第19.2章 高级 trait

本节我们将学习 trait 的一些高级用法,它们包括

  1. 关联类型
  2. 默认泛型类型参数
  3. 运算符重载
  4. 完全限定语法 <Type as Trait>::function(receiver_if_method, next_arg, ...);
  5. trait 中需要用到另一个 trait 的某个功能,说白了需要能够依赖相关的 trait 也被实现。
  6. newtype 模式(注意是模式,不是关键字)用于在外部crate 上实现 外部trait

关联类型

关联类型是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。
trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。

一个带有关联类型的 trait 的例子是标准库提供的 Iterator trait。它有一个叫做 Item 的关联类型来替代遍历的值的类型。

1
2
3
4
5
pub trait Iterator {
    type Item; // Item 是一个占位类型,这个 trait 的实现者会指定 Item 的具体类型

    fn next(&mut self) -> Option<Self::Item>; // next 方法定义表明它返回 Option<Self::Item> 类型的值。
}

在实现时,当实现者指定了关联类型时,方法的返回值就是其指定的类型

1
2
3
4
5
6
7
impl Iterator for Counter {
    type Item = u32; // 指定关联类型为 u32

    fn next(&mut self) -> Option<Self::Item> {
        // --省略部分代码--
    }
}

这类似于泛型,那为什么不这么定义呢?

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

它和泛型有什么区别呢?感兴趣可以参考 Rust程序设计语言 示例 19-13: 一个使用泛型的 Iterator trait 假想定义

简单总结一下,关联类型,通常用于指定 trait 方法的返回值类型

默认泛型类型参数

当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <类型占位符名称=实际的类型>

1
2
3
4
5
6
7
// 定义泛型类型默认参数 Rhs=Self
// 如果实现 Add trait 时不指定 Rhs 的具体类型,Rhs 的类型将是默认的 Self 类型
trait Add<Rhs=Self> {
    type Output;

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

运算符重载

运算符重载是指在特定情况下自定义运算符(比如 +)行为的操作。 例如,我们想通过 + 运算符让两个 Point 结构体相加生成一个新的Point

上面的 Add trait 就是一个用于实现运算符重载的 trait,它可以重新定义特定情况下 + 运算符的行为,它位于 std::ops 包中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// 实现 Add trait,实现时没有指定Rhs的具体类型,因此它是Self,因为我们希望将两个 Point 实例相加。
impl Add for Point {
    type Output = Point; // 指定关联类型为 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 }, // 实现了 Add trait 后,就可以两个 Point 相加
        Point { x: 3, y: 3 }
    );
}

让我们看看一个实现 Add trait 时希望自定义 Rhs 类型而不是使用默认类型的例子。

下面这段程序,实现了 Millimeters 类型(代表毫米值) 和 Meters 类型(代表米值)相加,并让 Add 的实现正确处理转换。

你不用管什么米值毫米值什么意思,因为这不重要,重要的是要知道如何自定义Rhs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::ops::Add;

struct Millimeters(u32); // 这种将现有类型简单封装进另一个结构体的方式被称为 newtype 模式,这个后面再说,现在不用管
struct Meters(u32); // 这种将现有类型简单封装进另一个结构体的方式被称为 newtype 模式,这个后面再说,现在不用管

// 实现 Add trait, 为 Rhs 提供类型值 Meters
impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

为了使 MillimetersMeters 能够相加,我们指定 impl Add<Meters> 来设定 Rhs 类型参数的值而不是使用默认的 Self

完全限定语法

完全限定语法用于消除歧义,我认为应该尽量避免写出有歧义的代码,不过有时,可能实在没有更好的办法只能这样,我们来看看下面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Human {
    // Human 自己的 fly 方法
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

impl Pilot for Human {
    // 实现了 Pilot trait 的 flay 方法
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    // 实现了 Wizard trait 的 flay 方法
    fn fly(&self) {
        println!("Up!");
    }
}

调用时,默认执行的是 Human 自己的 fly 方法

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

1
2
3
4
fn main() {
    let person = Human;
    person.fly();
}

为了能够调用 Pilot traitWizard traitfly 方法,我们需要使用更明显的语法以便能指定我们指的是哪个 fly 方法。

1
2
3
4
5
6
fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly(); // 等同于 Human::fly(&person)
}

运行这段代码会打印出

1
2
3
4
5
6
7
$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

因为 fly 方法获取一个 self 参数,而 方法 第一个参数就是 self,Rust 可以根据 self 的类型计算出应该使用哪一个 trait 实现。

但我们知道 关联函数没有self参数,我们来看看下面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait Animal {
    fn baby_name() -> String; // 关联函数
}

struct Dog;

impl Dog {
    // Dog 自己的关联函数
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    // 实现 Animal 的关联函数
    fn baby_name() -> String {
        String::from("puppy")
    }
}

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

运行这段代码会打印出

1
2
3
4
5
$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

可以看到它直接调用了定义于 Dog 之上的关联函数。那如果我们想调用 Animal 上的 baby_name

我们可以使用完全限定语法,它的语法是

1
<Type as Trait>::function(receiver_if_method, next_arg, ...);

对于关联函数,其没有一个 receiver,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。
然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。
只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。

我们在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 Dog 类型当作 Animal 对待,指定希望调用的是 DogAnimal trait 实现中的 baby_name

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

运行这段代码

1
2
3
4
5
$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

在 trait 中需要用到另一个 trait 的某个功能

有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。

例如我们希望创建一个带有 outline_print 方法的 trait OutlinePrint,它会打印出带有星号框的值。
也就是说,如果 Point 实现了 Display 并返回 (x, y),调用以 1 作为 x3 作为 yPoint 实例的 outline_print 会显示如下:

1
2
3
4
5
**********
*        *
* (1, 3) *
*        *
**********

可以通过在 trait 定义中指定 OutlinePrint: Display 来做到这一点。如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::fmt;

// 指定 OutlinePrint 依赖 fmt::Display ,这类似 trait bounds
trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string(); // 因为指定了 OutlinePrint 需要 Display trait,则可以在 outline_print 中使用 to_string
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

如果尝试在一个没有实现 Display 的类型上实现 OutlinePrint 则会得到一个错误说 Display 是必须的而未被实现。

newtype 模式用于在外部crate 上实现外部 trait

这里,书上说了一大堆废话,我是没看懂什么意思,这里直接摘录重要部分

例如,如果想要在 Vec<T> 上实现 Display,而孤儿规则阻止我们直接这么做,因为 Display traitVec<T> 都定义于我们的 crate 之外。
可以创建一个包含 Vec<T> 实例的 Wrapper 结构体,接着可以在 Wrapper 上实现 Display 并使用 Vec<T> 的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::fmt;

struct Wrapper(Vec<String>); // newtype 模式

impl fmt::Display for Wrapper {
    // Display trait 只有一个方法就是 fmt
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", ")) // Display 的实现使用 self.0 来访问其内部的 Vec<T>,因为 Wrapper 是元组结构体而 Vec<T> 是结构体总位于索引 0 的项。
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

运行这段代码

1
2
3
4
5
    Blocking waiting for file lock on build directory
   Compiling rust_test v0.1.0 (/home/w/data/code/rust_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `/home/w/data/code/rust_test/target/debug/rust_test`
w = [hello, world]

此方法的缺点是,因为 Wrapper 是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper 上实现 Vec<T> 的所有方法
这样就可以代理到 self.0 上,这就允许我们完全像 Vec<T> 那样对待 Wrapper
如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref trait 并返回其内部类型是一种解决方案。
如果不希望封装类型拥有所有内部类型的方法,比如为了限制封装类型的行为,则必须只自行实现所需的方法。

updatedupdated2025-03-012025-03-01