【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
值分别相加来创建一个新的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
方法的 traitOutlinePrint
,它会将给定的值格式化为带有星号框。也就是说,给定一个实现了标准库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
的功能了。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。