Rust 学习笔记:高级 trait

Rust 学习笔记:高级 trait

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

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

具有关联类型的 trait 的一个例子是标准库提供的 Iterator trait。关联的类型名为 Item,代表实现 Iterator trait 的类型所迭代的值的类型。Iterator trait 的定义如下所示:

pub trait Iterator {
    type Item;

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

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

关联类型可能看起来与泛型类似,因为后者允许我们在定义函数时不指定它可以处理什么类型。为了检验这两个概念之间的区别,我们来看看一个名为 Counter 的类型上 Iterator trait 的实现,该类型指定 Item 类型为 u32:

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--

这种语法似乎与泛型类似。那么为什么不直接用泛型定义 Iterator trait 呢?

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

不同之处在于,当使用泛型时,我们必须在每个实现中注释类型;因为我们也可以为 Counter 或任何其他类型实现 Iterator<String>,所以我们可以为 Counter 实现多个 Iterator。换句话说,当 trait 具有泛型参数时,它可以为一个类型实现多次,每次都更改泛型类型参数的具体类型。当我们在 Counter 上使用 next 方法时,必须提供类型注释来指示我们想要使用的 Iterator 的哪个实现。

对于关联类型,我们不需要对类型进行注释,因为我们不能在一个类型上多次实现 trait。我们只能选择一次 Item 的类型,因为 Counter 只能有一个 impl Iterator。我们无需在使用中注明类型,例如,我们不必指定在 Counter 上调用 next 的所有地方都需要 u32 值的迭代器。

关联类型使 trait 定义更清晰,约定更严格。关联类型也成为 trait 接口的一部分:trait 的实现者必须提供一个类型来代替关联的类型占位符。

关联类型通常有一个描述如何使用该类型的名称,在 API 文档中记录关联类型是一种很好的做法。

默认泛型类型参数和操作符重载

当使用泛型类型参数时,可以为泛型类型指定默认的具体类型。如果默认类型有效,那么 trait 的实现者就不需要指定具体类型。使用 <PlaceholderType=ConcreteType> 语法,在声明泛型类型时指定默认类型。

这种技术很有用的一个很好的例子是操作符重载,在这种情况下,你可以在特定情况下自定义操作符(例如 +)的行为。

Rust 不允许创建自己的操作符或重载任意操作符。但是,你可以通过实现与操作符相关的 trait 来重载 std::ops 中列出的操作和相应的特征。

例如,我们重载 + 操作符将两个 Point 实例相加。我们通过在 Point 结构体上实现 Add 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 }
    );
}

add 方法添加两个 Point 实例的 x 值和两个 Point 实例的 y 值来创建一个新的 Point。Add trait 有一个名为 Output 的关联类型,该类型决定了 Add 方法返回的类型。

这段代码中的默认泛型类型在 Add trait 中。以下是它的定义:

trait Add<Rhs=Self> {
    type Output;

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

这段代码看起来应该很熟悉:一个带有一个方法和一个关联类型的 trait。新的部分是 Rhs=Self:这种语法称为默认类型参数。Rhs 泛型类型参数(“right-hand side”的缩写)定义了 add 方法中 rhs 参数的类型。如果我们在实现 Add trait 时没有为 Rhs 指定一个具体的类型,那么 Rhs 的类型将默认为 Self,这将是我们实现 Add 的类型。

当我们为 Point 实现 Add 时,我们使用了 Rhs 的默认值,因为我们想要使两个 Point 实例相加。

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

我们有两个结构体,Millimeters 和 Meters,以不同的单位保存值。我们希望将以毫米为单位的值添加到以米为单位的值,并让 add 的实现正确地进行转换。我们可以用 Meters 作为 Rhs 为 Millimeters 实现 Add。

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

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

使用默认类型参数有两种目的:

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

消除同名方法之间的歧义

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

当调用同名的方法时,你需要告诉 Rust 你想使用哪一个。

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

当我们在 Human 的实例上调用 fly 时,编译器默认调用在 Human 类型上直接实现的方法。

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

要从 Pilot trait 或 Wizard trait 调用 fly 方法,我们需要使用更显式的语法来指定我们指的是哪个 fly 方法。

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

在方法名之前指定 trait 名向 Rust 澄清了我们想要调用哪个实现。因为 fly 方法接受一个 self 参数,如果我们有两个类型,它们都实现了一个 trait,那么 Rust 就可以根据 self 的类型找出要使用哪个 trait 的实现。

我们还可以编写 Human::fly(&person),它相当于 person.fly(),但是如果我们不需要消除歧义,那么编写这个代码就有点长了。

然而,非方法的关联函数没有 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());
}

程序打印 A baby dog is called a Spot,说明 Dog::baby_name 函数直接调用在 Dog 类型上定义的相关函数。

这个输出不是我们想要的。我们想要调用的 baby_name 函数是我们在 Dog 上实现的 Animal trait 的一部分。

指定 trait 名称的技术在这里没有帮助,如果我们将 main 更改为下列代码,我们将得到一个编译错误。

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

在这里插入图片描述

为了消除歧义,并告诉 Rust 我们希望为 Dog 使用 Animal 的实现,而不是为其他类型使用 Animal 的实现,我们需要使用完全限定语法。

将代码修改为:

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

我们在尖括号内为 Rus t提供了一个类型注释,这表明我们想要从在 Dog 上实现的 Animal trait 中调用 baby_name 方法。

现在,程序打印 A baby dog is called a puppy。

一般来说,完全限定语法的定义如下:

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

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

使用 Supertrait

在 Rust 中,如果一个 trait 依赖于另一个 trait 实现的功能,我们可以将这个被依赖的 trait 作为 Supertrait 引入。这让我们可以在 trait 内部直接使用另一个 trait 提供的功能。

假设我们创建一个 OutlinePrint trait,该 trait 的 outline_print 方法将打印一个用星号框起来的给定值。也就是说,给定一个 Point 结构体,该结构体实现了标准库的 Display trait,当我们在一个 x 为 1、y 为 3 的 Point 实例上调用 outline_print 方法时,它应该打印以下内容:

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

在 outline_print 方法的实现中,我们希望使用 Display trait 的功能。因此,我们需要指定 OutlinePrint trait 只适用于同时实现 Display 并提供 OutlinePrint 所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display 来做到这一点。这种技术类似于在 trait 上添加一个绑定的 trait。

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 需要 Display trait,所以我们可以使用 to_string 函数,该函数是为任何实现 Display 的类型自动实现的。如果我们在 OutlinePrint 后没有添加 : fmt::Display,就使用 to_string 函数,我们会得到一个错误,说在当前作用域中没有为类型 &Self 找到名为 to_string 的方法。

让我们看看当我们尝试在一个不实现 Display 的类型上实现 OutlinePrint 会发生什么,比如 Point 结构体:

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

我们得到一个错误,说 Display 是 Point 必需的,但没有实现:

在这里插入图片描述

为了解决这个问题,我们在 Point 上实现了 Display,Display trait 要求实现一个 fmt 方法,最终满足了 OutlinePrint 要求的约束。

use std::fmt;

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

然后,在 Point 上实现 OutlinePrint trait 将成功编译,并且我们可以在 Point 实例上调用 outline_print 方法。

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

我们之前提到了孤儿规则:我们允许在一个类型上实现 trait,当且仅当这个 trait 或这个类型,或者两者都是我们本地 crate 实现的。即不允许在外部类型上实现外部 trait。

使用 newtype 模式可以绕过这个限制,这涉及到在元组结构中创建一个新类型。元组结构将有一个字段,并且是我们想要实现 trait 的类型的薄包装。包装器类型是我们的 crate 的本地类型,我们可以在包装器上实现 trait。

例如,假设我们想要在 Vec<T> 上实现 Display,孤立规则阻止我们直接这样做,因为 Display trait 和 Vec<T> 类型是在我们的 crate 之外定义的。我们可以创建一个 Wrapper 结构体来保存 Vec<T> 的实例,然后我们可以在 Wrapper 上实现 Display 并使用 Vec<T> 值。

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

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

使用这种技术的优点是没有运行时性能损失,包装器类型在编译时被忽略。

使用这种技术的缺点是 Wrapper 是一种新类型,因此它没有它所保存的值的方法。我们必须直接在 Wrapper 上实现 Vec<T> 的所有方法,以便这些方法委托给 self.0,这将允许我们完全像对待 Vec<T> 一样对待 Wrapper。

如果我们希望新类型拥有内部类型拥有的所有方法,在包装器上实现 Deref trait 来返回内部类型将是一个解决方案。如果我们不希望 Wrapper 类型拥有内部类型的所有方法(例如,为了限制 Wrapper 类型的行为),我们就必须手动实现我们想要的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值