0


【Rust】——高级trait

💻博主现有专栏:

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

** **Y小夜-CSDN博客

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

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

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

    一个带有关联类型的 trait 的例子是标准库提供的 
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

值分别相加来创建一个新的

Point

Add

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 的 **父(超)trait**(*supertrait*)。

    例如我们希望创建一个带有 
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

的功能了。


本文转载自: https://blog.csdn.net/shsjssnn/article/details/136857047
版权归原作者 Y小夜 所有, 如有侵权,请联系我们删除。

“【Rust】——高级trait”的评论:

还没有评论