【Rust】——高级trait

Y小夜 2024-08-22 11:35:07 阅读 62

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等

🥏主页链接:

                Y小夜-CSDN博客

目录

🎯关联类型在trait定义中指定占位符类型

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

🎯完全限定语法与消歧义:调用相同名称的方法

🎯完全限定语法与消歧义:调用相同名称的方法

🎯父trait用于在另一个trait中使用trait的功能

🎯newtype模式用以在外部类型上实现外部trait


🎯关联类型在trait定义中指定占位符类型

        关联类型associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。

        关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。

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

pub trait Iterator {

type Item;

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

}

  Item 是一个占位符类型,同时 next 方法定义表明它返回 Option<Self::Item> 类型的值。这个 trait 的实现者会指定 Item 的具体类型,然而不管实现者指定何种类型,next 方法都会返回一个包含了此具体类型值的 Option

        关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。让我们通过在一个 Counter 结构体上实现 Iterator trait 的例子来检视其中的区别。

impl Iterator for Counter {

type Item = u32;

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

// --snip--

pub trait Iterator<T> {

fn next(&mut self) -> Option<T>;

}

        区别在于当如示例 19-13 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 Iterator<String> for Counter,或任何其他类型,这样就可以有多个 Counter 的 Iterator 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 Counter 的 next 方法时,必须提供类型注解来表明希望使用 Iterator 的哪一个实现。

        通过关联类型,则无需标注类型,因为不能多次实现这个 trait。对于示例 19-12 使用关联类型的定义,我们只能选择一次 Item 会是什么类型,因为只能有一个 impl Iterator for Counter。当调用 Counter 的 next 时不必每次指定我们需要 u32 值的迭代器。

        关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符。关联类型通常有一个描述类型用途的名字,并且在 API 文档中为关联类型编写文档是一个最佳实践。

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

        当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>

        这种情况的一个非常好的例子是使用 运算符重载Operator overloading),这是指在特定情况下自定义运算符(比如 +)行为的操作。

        Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 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 值和 y 值分别相加来创建一个新的 PointAdd trait 有一个叫做 Output 的关联类型,它用来决定 add 方法的返回值类型。

trait Add<Rhs=Self> {

type Output;

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

}

        这些代码看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 Rhs=Self:这个语法叫做 默认类型参数default type parameters)。Rhs 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数。如果实现 Add trait 时不指定 Rhs 的具体类型,Rhs 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型。

        当为 Point 实现 Add 时,使用了默认的 Rhs,因为我们希望将两个 Point 实例相加。让我们看看一个实现 Add trait 时希望自定义 Rhs 类型而不是使用默认类型的例子。

        默认参数类型主要用于如下两个方面:

扩展类型而不破坏现有代码。在大部分用户都不需要的特定情况进行自定义。

        标准库的 Add trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 Add trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。

        第一个目的是相似的,但过程是反过来的:如果需要为现有 trait 增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。

🎯完全限定语法与消歧义:调用相同名称的方法

        Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的!

        不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。考虑一下代码,这里定义了 trait Pilot 和 Wizard 都拥有方法 fly。接着在一个本身已经实现了名为 fly 方法的类型 Human 上实现这两个 trait。每一个 fly 方法都进行了不同的操作:

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

}

}

🎯完全限定语法与消歧义:调用相同名称的方法

        Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的!

不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。这里定义了 trait Pilot 和 Wizard 都拥有方法 fly。接着在一个本身已经实现了名为 fly 方法的类型 Human 上实现这两个 trait。每一个 fly 方法都进行了不同的操作:

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 时,编译器默认调用直接实现在类型上的方法:

fn main() {

let person = Human;

person.fly();

}

🎯父trait用于在另一个trait中使用trait的功能

        有时我们可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的 父(超)traitsupertrait)。

        例如我们希望创建一个带有 outline_print 方法的 trait OutlinePrint,它会将给定的值格式化为带有星号框。也就是说,给定一个实现了标准库 Display trait 的并返回 (x, y) 的 Point,当调用以 1 作为 x 和 3 作为 y 的 Point 实例的 outline_print 会显示如下:

**********

* *

* (1, 3) *

* *

**********

        在 outline_print 的实现中,因为希望能够使用 Display trait 的功能,则需要说明 OutlinePrint 只能用于同时也实现了 Display 并提供了 OutlinePrint 需要的功能的类型。可以通过在 trait 定义中指定 OutlinePrint: Display 来做到这一点。这类似于为 trait 增加 trait bound。示例 19-22 展示了一个 OutlinePrint 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,则可以在 outline_print 中使用 to_string,其会为任何实现 Display 的类型自动实现。如果不在 trait 名后增加 : Display 并尝试在 outline_print 中使用 to_string,则会得到一个错误说在当前作用域中没有找到用于 &Self 类型的方法 to_string

struct Point {

x: i32,

y: i32,

}

impl OutlinePrint for Point {}

$ 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:6

|

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`

For more information about this error, try `rustc --explain E0277`.

error: could not compile `traits-example` due to previous error

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

        我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。

        这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。Newtype 是一个源自 Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。

        例如,如果想要在 Vec<T> 上实现 Display,而孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。可以创建一个包含 Vec<T> 实例的 Wrapper 结构体,接着可以如列表 19-23 那样在 Wrapper 上实现 Display 并使用 Vec<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);

}

   Display 的实现使用 self.0 来访问其内部的 Vec<T>,因为 Wrapper 是元组结构体而 Vec<T> 是结构体总位于索引 0 的项。接着就可以使用 Wrapper 中 Display 的功能了。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。