Rust 标准库特征指南
原文 / 译者:skanfd
目录
- 引入 Intro
- 特性的基础知识 Trait Basics
- 可自动实现的特性 Auto Traits
- 常用特性 General Traits
- 文本格式化特性 Formatting Traits
- 算符重载特性 Operator Traits
- 转换特性 Conversion Traits
- 错误处理 Error Handling
- 转换特性深入 Conversion Traits Continued
- 迭代特性 Iteration Traits
- 输入输出特性 I/O Traits
- 结语 Conclusion
- 讨论 Discuss
- 通告 Notifications
- 更多资料 Further Reading
- 翻译 Translation
引入 Intro
你是否曾对以下特性的区别感到困惑:
Deref<Target = T>
,AsRef<T>
和Borrow<T>
?Clone
,Copy
和ToOwned
?From<T>
和Into<T>
?TryFrom<&str>
和FromStr
?FnOnce
,FnMut
,Fn
和fn
?
或者有这样的疑问:
- “我应该在特性中使用关联类型还是泛型类型?”
- "什么是通用泛型实现?"
- "子特性与超特性是如何工作的?"
- "为什么某个特性没有实现任何方法?"
本文正是为你解答以上困惑而撰写!而且本文绝不仅仅只回答了以上问题。下面,我们将一起对 Rust 标准库中所有最流行、最常用的特性做一个走马观花般的概览!
你可以按顺序阅读本文,也可以直接跳读至你最感兴趣的特性。每节都会提供预备知识列表,它会帮助你获得相应的背景知识,不必担心跳读带来的理解困难。
特性的基础知识
本章覆盖了特性的基础知识,相应内容在以后的章节中不再赘述。
特性的记号
特性的记号指的是,在特性的声明中可使用的记号。
Self
Self
永远引用正被实现的类型。
函数
特性的函数指的是,任何不以 self
关键字作为首参数的函数。
特性的函数同时声明在特性本身以及具体实现类型的命名空间中。
fn main() {
let zero: i32 = Default::default();
let zero = i32::default();
}
方法
特性的方法指的是,任何以 self
关键字作为首参数的函数,其类型是 Self
, &Self
或 &mut Self
。前者的类型也可以包裹在 Box
, Rc
, Arc
或 Pin
中。
可以使用点算符在具体实现类型上调用方法:
fn main() {
let five = 5.to_string();
}
并且,与函数相似地,方法也声明在特性本身以及具体实现类型的命名空间中。
fn main() {
let five = ToString::to_string(&5);
let five = i32::to_string(&5);
}
关联类型
特性内部可以声明关联类型。当我们希望在特性函数的签名中使用某种 Self
以外的类型,又不希望硬编码这种类型,而是希望后来的实现该特性的程序员来选择该类型具体是什么的时候,关联类型会很有用。
trait Trait {
type AssociatedType;
fn func(arg: Self::AssociatedType);
}
struct SomeType;
struct OtherType;
// any type implementing Trait can
// choose the type of AssociatedType
// 我们可以在实现 Trait 特性的时候
// 再决定 AssociatedType 的具体类型
// 而不必是在声明 Trait 特性的时候
impl Trait for SomeType {
type AssociatedType = i8; // chooses i8
fn func(arg: Self::AssociatedType) {}
}
impl Trait for OtherType {
type AssociatedType = u8; // chooses u8
fn func(arg: Self::AssociatedType) {}
}
fn main() {
SomeType::func(-1_i8); // can only call func with i8 on SomeType
OtherType::func(1_u8); // can only call func with u8 on OtherType
// 同一特性实现在不同类型上时,可以具有不同的函数签名
}
泛型参数
“泛型参数” 是泛型类型参数、泛型寿命参数以及泛型常量参数的统称。由于这些术语过于佶屈聱牙,我们通常将他们缩略为“泛型类型”,“泛型寿命”和“泛型常量”。鉴于标准库中的特性无一采用泛型常量,本文也略过不讲。
我们可以使用以下参数来声明特性:
可以为泛型类型指定默认值,最常用的默认值是 Self
,此外任何其它类型都是可以的。
不仅可以为特性提供泛型,也可以独立地为函数或方法提供泛型。
泛型类型与关联类型
通过使用泛型类型与关联类型,我们都可以将具体类型的选择问题抛给后来实现该特性的程序员来决定,这一节将解释我们如何在相似的两者之间做出选择。
按照惯常的经验:
- 对于某一特性,每个类型仅应当有单一实现时,使用关联类型。
- 对于某一特性,每个类型可以有多个实现时,使用泛型类型。
例如,我们声明一个 Add
特性,它允许将各值加总在一起。这是仅使用关联类型的初始设计:
trait Add {
type Rhs;
type Output;
fn add(self, rhs: Self::Rhs) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Rhs = Point;
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 1 };
let p2 = Point { x: 2, y: 2 };
let p3 = p1.add(p2);
assert_eq!(p3.x, 3);
assert_eq!(p3.y, 3);
}
例如,我们希望程序允许将 i32 类型的值与 Point 类型的值相加,其规则是该 i32 类型的值分别加到成员 x
与成员 y
。
trait Add {
type Rhs;
type Output;
fn add(self, rhs: Self::Rhs) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Rhs = Point;
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl Add for Point { // ❌
type Rhs = i32;
type Output = Point;
fn add(self, rhs: i32) -> Point {
Point {
x: self.x + rhs,
y: self.y + rhs,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 1 };
let p2 = Point { x: 2, y: 2 };
let p3 = p1.add(p2);
assert_eq!(p3.x, 3);
assert_eq!(p3.y, 3);
let p1 = Point { x: 1, y: 1 };
let int2 = 2;
let p3 = p1.add(int2); // ❌
assert_eq!(p3.x, 3);
assert_eq!(p3.y, 3);
}
编译出错:
error[E0119]: conflicting implementations of trait `Add` for type `Point`:
--> src/main.rs:23:1
|
12 | impl Add for Point {
| ------------------ first implementation here
...
23 | impl Add for Point {
| ^^^^^^^^^^^^^^^^^^ conflicting implementation for `Point`
由于 Add
特性未提供泛型类型,因而每个类型只能具有该特性的单一实现,这即是说一旦我们指定了 Rhs
和 Output
的类型后就不可再更改了!为了 Point 类型的值能同时接受 i32 类型和 Point 类型的值作为被加数,我们应当重构之以将 Rhs
从关联类型改为泛型类型,这将允许我们为 Rhs
指定不同的类型并为同一类型多次实现某一特性。
trait Add<Rhs> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add<Point> for Point {
type Output = Self;
fn add(self, rhs: Point) -> Self::Output {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl Add<i32> for Point { // ✅
type Output = Self;
fn add(self, rhs: i32) -> Self::Output {
Point {
x: self.x + rhs,
y: self.y + rhs,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 1 };
let p2 = Point { x: 2, y: 2 };
let p3 = p1.add(p2);
assert_eq!(p3.x, 3);
assert_eq!(p3.y, 3);
let p1 = Point { x: 1, y: 1 };
let int2 = 2;
let p3 = p1.add(int2); // ✅
assert_eq!(p3.x, 3);
assert_eq!(p3.y, 3);
}
例如,我们现在声明一个包含两个 Point
类型的新类型 Line
,要求当两个 Point
类型相加时返回 Line
而不是 Point
。在当前 Add
特性的设计中 Output
是关联类型,不能满足这一要求,重构之以将关联类型改为泛型类型:
trait Add<Rhs, Output> {
fn add(self, rhs: Rhs) -> Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add<Point, Point> for Point {
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl Add<i32, Point> for Point {
fn add(self, rhs: i32) -> Point {
Point {
x: self.x + rhs,
y: self.y + rhs,
}
}
}
struct Line {
start: Point,
end: Point,
}
impl Add<Point, Line> for Point { // ✅
fn add(self, rhs: Point) -> Line {
Line {
start: self,
end: rhs,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 1 };
let p2 = Point { x: 2, y: 2 };
let p3: Point = p1.add(p2);
assert!(p3.x == 3 && p3.y == 3);
let p1 = Point { x: 1, y: 1 };
let int2 = 2;
let p3 = p1.add(int2);
assert!(p3.x == 3 && p3.y == 3);
let p1 = Point { x: 1, y: 1 };
let p2 = Point { x: 2, y: 2 };
let l: Line = p1.add(p2); // ✅
assert!(l.start.x == 1 && l.start.y == 1 && l.end.x == 2 && l.end.y == 2)
}
所以说,哪一种 Add
特性最好?答案是具体问题具体分析!不管白猫黑猫,会捉老鼠就是好猫。
作用域
特性仅当被引入当前作用域时才可以使用。绝大多数的初学者要在编写 I/O 程序时经历一番痛苦挣扎后,才能领悟到这一点,原因是 Read
和 Write
两个特性并未包含在标准库的 prelude 模块中。
use std::fs::File;
use std::io;
fn main() -> Result<(), io::Error> {
let mut file = File::open("Cargo.toml")?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?; // ❌ read_to_string not found in File
// ❌ 当前文件中找不到 read_to_string
Ok(())
}
read_to_string(buf: &mut String)
声明于 std::io::Read
特性,并实现于 std::fs::File
类型,若要调用该函数还须得 std::io::Read
特性处于当前作用域中:
use std::fs::File;
use std::io;
use std::io::Read; // ✅
fn main() -> Result<(), io::Error> {
let mut file = File::open("Cargo.toml")?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?; // ✅
Ok(())
}
诸如 std::prelude::v1
,prelude 是标准库的一类模块,其特点是该模块命名空间下的成员将被自动导入到任何其它模块的顶部,其作用等效于 use std::prelude::v1::*
。因此,以下 prelude 模块中的特性无需我们显式导入,它们永远存在于当前作用域:
- AsMut
- AsRef
- Clone
- Copy
- Default
- Drop
- Eq
- Fn
- FnMut
- FnOnce
- From
- Into
- ToOwned
- IntoIterator
- Iterator
- PartialEq
- PartialOrd
- Send
- Sized
- Sync
- ToString
- Ord
衍生宏
标准库导出了一系列实用的衍生宏,我们可以利用它们方便快捷地为特定类型实现某种特性,前提是该类型的成员亦实现了相应的特性。衍生宏与它们各自所实现的特性同名:
用例:
注意:衍生宏仅是一种机械的过程,宏展开之后发生的事情并无一定之规。并没有绝对的规定要求衍生宏展开之后必须要为类型实现某种特性,又或者它们必须要求该类型的所有成员都必须实现某种特性才能为当前类型实现该特性,这仅仅是在标准库衍生宏的编纂过程中逐渐约定俗成的规则。
默认实现
特性可为函数与方法提供默认的实现。
trait Trait {
fn method(&self) {
println!("default impl");
}
}
struct SomeType;
struct OtherType;
// use default impl for Trait::method
// 省略时使用默认实现
impl Trait for SomeType {}
impl Trait for OtherType {
// use our own impl for Trait::method
// 重写时覆盖默认实现
fn method(&self) {
println!("OtherType impl");
}
}
fn main() {
SomeType.method(); // prints "default impl"
OtherType.method(); // prints "OtherType impl"
}
这对于实现特性中某些仅依赖于其它方法的方法来说极其方便。
trait Greet {
fn greet(&self, name: &str) -> String;
fn greet_loudly(&self, name: &str) -> String {
self.greet(name) + "!"
}
}
struct Hello;
struct Hola;
impl Greet for Hello {
fn greet(&self, name: &str) -> String {
format!("Hello {}", name)
}
// use default impl for greet_loudly
// 省略时使用 greet_loudly 的默认实现
}
impl Greet for Hola {
fn greet(&self, name: &str) -> String {
format!("Hola {}", name)
}
// override default impl
// 重写时覆盖 greet_loudly 的默认实现
fn greet_loudly(&self, name: &str) -> String {
let mut greeting = self.greet(name);
greeting.insert_str(0, "¡");
greeting + "!"
}
}
fn main() {
println!("{}", Hello.greet("John")); // prints "Hello John"
println!("{}", Hello.greet_loudly("John")); // prints "Hello John!"
println!("{}", Hola.greet("John")); // prints "Hola John"
println!("{}", Hola.greet_loudly("John")); // prints "¡Hola John!"
}
标准库中的许多特性都为它们的方法提供默认实现。
通用泛型实现
通用泛型实现是对泛型类型的实现,与之对应的是对特定类型的实现。我们将以 is_even 方法为例说明如何对数字类型实现通用泛型实现。
显而易见地,我们重复实现了近乎相同的逻辑,这非常的繁琐。进一步来讲,如果 Rust 在将来决定增加更多的数字类型(小概率事件并非绝不可能),那么我们将不得不重新回到这里对新增的数字类型编写代码。通用泛型实现恰可以解决这些问题:
默认实现可以重写,而通用泛型实现不可重写。
编译出错:
error[E0119]: conflicting implementations of trait `Even` for type `u8`:
--> src/lib.rs:22:1
|
10 | / impl<T> Even for T
11 | | where
12 | | T: Rem<Output = T> + PartialEq<T> + Sized,
13 | | u8: TryInto<T>,
... |
19 | | }
20 | | }
| |_- first implementation here
21 |
22 | impl Even for u8 {
| ^^^^^^^^^^^^^^^^ conflicting implementation for `u8`
重叠的实现产生了冲突,于是 Rust 拒绝了该代码以确保特性一致性。特性一致性指的是,对任意给定类型,仅能对某一特性具有单一实现。Rust 强制实现特性一致性,而这一规则的潜在影响与变通方法超出了本文的讨论范围。
子特性与超特性
子特性的“子”即为子集,超特性的“超”即为超集。若有下列特性声明:
所有实现了子特性的类型都是实现了超特性的类型的子集,也可以说,所有实现了超特性的类型都是实现了子特性的类型的超集。
以上代码等价于:
这是一种易于忽略但又至关重要的区别 —— 约束是 Self
的约束,而不是 Subtrait
的约束。后者没有任何意义,因为特性约束只能应用于具体类型。不能用一种特性去实现其它特性:
trait Supertrait {
fn method(&self) {
println!("in supertrait");
}
}
trait Subtrait: Supertrait {
// this looks like it might impl or
// override Supertrait::method but it
// does not
// 这可能会令你产生超特性的方法被覆盖的错觉(实际不会)
fn method(&self) {
println!("in subtrait")
}
}
struct SomeType;
// adds Supertrait::method to SomeType
impl Supertrait for SomeType {}
// adds Subtrait::method to SomeType
impl Subtrait for SomeType {}
// both methods exist on SomeType simultaneously
// neither overriding or shadowing the other
// 两个同名方法同时存在于同一类型时,既不重写也不影射
fn main() {
SomeType.method(); // ❌ ambiguous method call
// ❌ 不允许语义模糊的函数调用
// must disambiguate using fully-qualified syntax
// 必须使用完全限定的记号来明确你要使用的函数
<SomeType as Supertrait>::method(&st); // ✅ prints "in supertrait"
<SomeType as Subtrait>::method(&st); // ✅ prints "in subtrait"
}
此外,对于特定类型如何同时实现子特性与超特性并没有规定。子、超特性之间的方法也可以相互调用。
trait Supertrait {
fn super_method(&mut self);
}
trait Subtrait: Supertrait {
fn sub_method(&mut self);
}
struct CallSuperFromSub;
impl Supertrait for CallSuperFromSub {
fn super_method(&mut self) {
println!("in super");
}
}
impl Subtrait for CallSuperFromSub {
fn sub_method(&mut self) {
println!("in sub");
self.super_method();
}
}
struct CallSubFromSuper;
impl Supertrait for CallSubFromSuper {
fn super_method(&mut self) {
println!("in super");
self.sub_method();
}
}
impl Subtrait for CallSubFromSuper {
fn sub_method(&mut self) {
println!("in sub");
}
}
struct CallEachOther(bool);
impl Supertrait for CallEachOther {
fn super_method(&mut self) {
println!("in super");
if self.0 {
self.0 = false;
self.sub_method();
}
}
}
impl Subtrait for CallEachOther {
fn sub_method(&mut self) {
println!("in sub");
if self.0 {
self.0 = false;
self.super_method();
}
}
}
fn main() {
CallSuperFromSub.super_method(); // prints "in super"
CallSuperFromSub.sub_method(); // prints "in sub", "in super"
CallSubFromSuper.super_method(); // prints "in super", "in sub"
CallSubFromSuper.sub_method(); // prints "in sub"
CallEachOther(true).super_method(); // prints "in super", "in sub"
CallEachOther(true).sub_method(); // prints "in sub", "in super"
}
通过以上示例,希望读者能够领会到,子特性与超特性之间的关系并未被一刀切的限制住。接下来我们将学习一种将所有这些复杂性巧妙地封装在一起的心智模型,在这之前我们先来回顾一下我们用来理解泛型类型与特性约束的关系的心智模型。
即便我们不知道这个函数的具体实现,我们仍旧可以有理有据地猜测 t.clone()
将在函数的某处被调用,因为当泛型类型被特性所约束的时候,会给人一种它依赖于该特性的强烈暗示。这就是一种理解泛型类型与特性约束的关系的心智模型,它简单且可凭直觉 —— 泛型类型依赖于它们的特性约束。
现在,让我们看看 Copy
特性的声明:
以上的记号和之前我们为泛型添加特性约束的记号非常相似,但是 Copy
却完全不依赖 Clone
。早前建立的心智模型现在不适用了。在我看来,理解子特性与超特性的关系的最简单和最优雅的心智模型莫过于 —— 子特性 改良 了超特性。
“改良”一词故意地预留了一些模糊的空间,它的具体含义在不同的上下文中有所不同:
- 子特性可能比超特性的方法更加特异化、运行更快或使用更少内存等等,例如
Copy: Clone
- 子特性可能比超特性的方法具有额外的功能,例如
Eq: PartialEq
,Ord: PartialOrd
和ExactSizeIterator: Iterator
- 子特性可能比超特性的方法更灵活和更易于调用,例如
FnMut: FnOnce
和Fn: FnMut
- 子特性可能扩展了超特性并添加了新的方法,例如
DoubleEndedIterator: Iterator
和ExactSizeIterator: Iterator
特性对象
如果说泛型给了我们编译时的多态性,那么特性对象就给了我们运行时的多态性。通过特性对象,我们可以允许函数在运行时动态地返回不同的类型。
特性对象也允许我们在集合中存储不同类型的值:
特性对象的结构体大小是未知的,所以必须要通过指针来引用它们。具体类型与特性对象在字面上的区别在于,特性对象必须要用 dyn
关键字来修饰前缀,了解了这一点我们可以轻松辨别二者。
并非全部的特性都可以转换为特性对象,一个 “对象安全” 的特性必须满足:
- 该特性不要求
Self: Sized
- 该特性的所有方法都是 “对象安全” 的
一个特性的方法若要是 “对象安全” 的,必须满足:
- 该方法要求
Self: Sized
- 该方法仅在接收参数中使用
Self
类型
关于具有这些限制条件的原因超出了本文的讨论范围且与下文无关,如果你对此深感兴趣不妨阅读 Sizedness in Rust 以了解详情。
仅用于标记的特性
仅用于标记的特性,即是某种声明体为空的特性。它们存在的意义在于 “标记” 所实现的类型,且该类型具有某种类型系统所无法表达的属性。
可自动实现的特性
可自动实现的特性指的是,存在这样一种特性,若给定类型的成员都实现了该特性,那么该类型就隐式地自动实现该特性。这里所说的 “成员” 依据上下文而具有不同的含义,包括而又不限于结构体的字段、枚举的变量、数组的元素和元组的内容等等。
所有可自动实现的特性都是仅用于标记的特性,反之则不是。正是由于可自动实现的特性必须是仅用于标记的特性,所以编译器才能够自动为其提供一个默认实现,反之编译器就无能为力了。
可自动实现的特性的示例:
不安全的特性
以 unsafe
修饰前缀的特性,意味着该特性的实现可能需要不安全的代码。Send
特性与 Sync
特性以 unsafe
修饰前缀意味着,如果特定类型没有自动实现该特性,那么说明该类型的成员并非都实现了该特性,这提示着我们手动实现该特性一定要谨慎小心,以确保没有发生数据竞争。
可自动实现的特性
Send & Sync
预备知识
实现 Send
特性的类型可以安全地往返于多线程。实现 sync
特性的类型,其引用可以安全地往返于多线程。用更加准确的术语来讲,当且仅当 &T
实现 Send
特性时,T
才能实现 Sync
特性。
几乎所有类型都实现了 Send
特性和 Sync
特性。对于 Send
唯一需要注意的例外是 Rc
,对于 Sync
唯三需要注意的例外是 Rc
,Cell
和 RefCell
。如果我们需要 Send
版的 Rc
,可以使用 Arc
。如果我们需要 Sync
版的 Cell
或 RefCell
,可以使用 Mutex
或 RwLock
。尽管我们可以使用 Mutex
或 RwLock
来包裹住原语类型,但通常使用标准库提供的原子原语类型会更好,诸如 AtomicBool
,AtomicI32
和 AtomicUsize
等等。
多亏了 Rust 严格的借用规则,几乎所有的类型都是 Sync
的。这对于一些人来讲可能会很惊讶,但事实胜于雄辩,甚至对于那些没有内部同步机制的类型来说也是如此。
对于同一数据,我们可以放心地将该数据的多个不可变引用传递给多个线程,因为只要当前存在一个该数据的不可变引用,那么 Rust 就会静态地确保该数据不会被改变:
use crossbeam::thread;
fn main() {
let mut greeting = String::from("Hello");
let greeting_ref = &greeting;
thread::scope(|scoped_thread| {
// spawn 3 threads
// 产生三个线程
for n in 1..=3 {
// greeting_ref copied into every thread
// greeting_ref 被拷贝到每个线程
scoped_thread.spawn(move |_| {
println!("{} {}", greeting_ref, n); // prints "Hello {n}"
});
}
// line below could cause UB or data races but compiler rejects it
// 下面这行代码可能导致数据竞争,于是编译器拒绝了它
greeting += " world";
// ❌ cannot mutate greeting while immutable refs exist
// ❌ 当不可变引用存在时,不可以修改引用的数据
});
// can mutate greeting after every thread has joined
// 当所有的线程结束之后,可以修改数据
greeting += " world"; // ✅
println!("{}", greeting); // prints "Hello world"
}
同样地,我们可以将某个数据的单个可变引用传递给单个线程,在此过程中不必担心出现数据竞争,因为 Rust 静态地确保了不存在其它可变引用。以下数据即仅可通过已经存在的单个可变引用而改变:
use crossbeam::thread;
fn main() {
let mut greeting = String::from("Hello");
let greeting_ref = &mut greeting;
thread::scope(|scoped_thread| {
// greeting_ref moved into thread
// greeting_ref 移动到当前线程
scoped_thread.spawn(move |_| {
*greeting_ref += " world";
println!("{}", greeting_ref); // prints "Hello world"
});
// line below could cause UB or data races but compiler rejects it
// 下面这行代码可能导致数据竞争,于是编译器拒绝了它
greeting += "!!!";
// ❌ cannot mutate greeting while mutable refs exist
// ❌ 可变引用存在时不可改变数据
});
// can mutate greeting after the thread has joined
// 当所有的线程结束之后,可以修改数据
greeting += "!!!"; // ✅
println!("{}", greeting); // prints "Hello world!!!"
}
这就是为什么绝大多数的类型都是 Sync 的而不需要实现任何显式的同步机制。对于数据 T ,如果我们试图从多个线程同时修改的话,编译器会对我们作出警告,除非我们将数据包裹在 Arc<Mutex<T>>
或 Arc<RwLock<T>>
中。所以说,当我们真的需要显式的同步机制时,编译器会强制要求我们这样做的。
Sized
预备知识
如果一个类型实现了 Sized
,那么说明该类型具体大小的字节数在编译时可以确定,并且也就说明该类型的实例可以存放在栈上。
类型的大小以及其所带来的潜在影响,是一个易于忽略但是又十分宏大的话题,它深刻地影响着本门语言的诸多方面。鉴于它的重要性,我已经写了一整篇文章(Sizedness in Rust)来具体阐述其内容,我高度推荐对于希望深入 sizedness 的人阅读此篇文章。下面是此篇文章的要点:
- 所有的泛型类型都具有隐式的
Sized
约束。
- 由于所有的泛型类型都具有隐式的
Sized
约束,如果我们希望摆脱这样的隐式约束,那么我们需要使用特殊的 “宽松约束” 记号?Sized
,目前这样的记号仅适用于Sized
特性:
- 所有的特性都具有隐式的
?Sized
约束。
这就是为什么特性对象可以实现具体特性。再次,向您推荐关于一切真相的Sizedness in Rust。
常用特性 General traits
Default
预备知识
为特定类型实现 Default
特性时,即为该类型赋予了可选的默认值。
这不仅利于快速原型设计,另外,在有时我们仅仅只是需要该类型的一个值,却完全不在意该值是什么的时候,这也非常方便。
fn main() {
// just give me some color!
let color = Color::default();
}
如此,我们可以明确地向该函数的用户传达出该函数某个参数的可选择性:
在泛型编程的语境中,Default
特性也可显其威力。
另外,我们在使用 update 记号构造结构体时也可享受到 Default
特性带来的便利。我们以 Color
结构的 new
构造器函数为例,它接受该结构的全部成员作为参数:
考虑以下更加便捷的构造器函数 —— 它仅接受该结构的部分成员作为参数,其它未指定的成员则回落到默认值:
Default
特性也可以用衍生宏的方式来实现:
Clone
预备知识
对于实现了 Clone
特性的类型,我们可以将一个不可变的引用转换为自有的类型,比如 &T
-> T
。Clone
特性对于这种转换的效率不做出保证,所以这样的转换速度可能很慢,代价可能很昂贵。
Clone
特性也有利于在泛型编程的语境中构造类型。请看下例:
克隆确是一个可以逃避借用检查器的好方法。倘若我们编写的代码无法通过借用检查,那么不妨通过克隆将这些引用转换为自有类型。
如果性能因素微不足道,我们不必羞于使用克隆。Rust 是一门底层语言,人们可以自由地控制程序行为的方方面面,这就很容易令人陷入盲目追求优化的陷阱,而不是专注于着手解决问题。对此我给出的建议是:正确第一,优雅第二,性能第三。只有程序初具雏形后,性能瓶颈的问题才可能凸显,这时我们再解决性能问题也不迟。与其说这是一条编程建议,更不如说这是一条人生建议,万事万物大抵如此,如果你现在不信,总有一天你会的。
Copy
预备知识
对于实现了 Copy
特性的类型,我们可以拷贝它,即 T
-> T
。Copy
特性确保了拷贝操作是按位的拷贝,所以它更快更高效。Copy
特性不可手动实现,必须由编译器提供其实现。注意:当使用衍生宏为类型实现 Copy
特性时,必须同时使用 Clone
衍生宏,因为 Copy
是 Clone
的子特性:
Copy
改良了 Clone
。克隆操作可能速度缓慢且代价昂贵,但是拷贝操作一定是高效低耗的,可以说拷贝就是一种物美价廉的克隆。Copy
特性的实现会令 Clone
特性的实现变得微不足道:
实现了 Copy
特性的类型,其在移动时的行为会发生变化。默认情况下,所有的类型都具有 移动语义 ,但是一旦该类型实现了 Copy
特性,则会变为 拷贝语义。 请考虑下例中语义的不同:
事实上,这两种语义背后执行的操作是完全相同的,都是将 src
按位复制到 dest
。其不同在于,在移动语义下,借用检查器从此吊销了 src
的可用性,而在拷贝语义下,src
保持可用。
言而总之,拷贝就是移动,移动就是拷贝。它们在底层毫无二致,仅仅是借用检查器对待它们的方式不同。
对于移动行为来讲更具体的例子 —— 你可以将 src
想象为一个 Vec<i32>
,它的结构体大致如下:
执行 desc = src
的结果如下:
此时 src
和 dest
就都是同一数据的可变引用了,这可就糟tm的大糕了,所以借用检查器就吊销了 src
的可用性,一旦再次使用 src
就会引发编译错误。
对于拷贝行为来讲更具体的例子 —— 你可以将 src
想象为一个 Option<i32>
,它的结构体大致如下:
执行 desc = src
的结果如下:
此时两者同时可用!因为 Option<i32>
实现了 Copy
。
或许你已经注意到,令 Copy
特性成为可自动实现的特性在理论上是可行的。但是 Rust 语言的设计者认为,比之于在恰当时隐式地继承拷贝语义,显示地声明为拷贝语义更加的简单和安全。前者可能会导致 Rust 语言产生十分反人类的行为,也更容易出现 bug 。
Any
预备知识
Rust 的多态性风格本身是参数化的,但如果我们希望临时使用一种更贴近于动态语言的多态性风格,可以借用 Any
特性来模拟。我们不需要手动实现 Any
特性,因为该特性通常由通用泛型实现所实现。
对于 dyn Any
的特性对象,我们可以使用 downcast_ref::<T>()
或 downcast_mut::<T>()
来尝试解析出 T
。
use std::any::Any;
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn inc(&mut self) {
self.x += 1;
self.y += 1;
}
}
fn map_any(mut any: Box<dyn Any>) -> Box<dyn Any> {
if let Some(num) = any.downcast_mut::<i32>() {
*num += 1;
} else if let Some(string) = any.downcast_mut::<String>() {
*string += "!";
} else if let Some(point) = any.downcast_mut::<Point>() {
point.inc();
}
any
}
fn main() {
let mut vec: Vec<Box<dyn Any>> = vec![
Box::new(0),
Box::new(String::from("a")),
Box::new(Point::default()),
];
// vec = [0, "a", Point { x: 0, y: 0 }]
vec = vec.into_iter().map(map_any).collect();
// vec = [1, "a!", Point { x: 1, y: 1 }]
}
这个特性鲜少被使用,因为参数化的多态性时常要优于这样变通使用的多态性,且后者也可以使用更加类型安全和更加直接的枚举来模拟。如下例:
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn inc(&mut self) {
self.x += 1;
self.y += 1;
}
}
enum Stuff {
Integer(i32),
String(String),
Point(Point),
}
fn map_stuff(mut stuff: Stuff) -> Stuff {
match &mut stuff {
Stuff::Integer(num) => *num += 1,
Stuff::String(string) => *string += "!",
Stuff::Point(point) => point.inc(),
}
stuff
}
fn main() {
let mut vec = vec![
Stuff::Integer(0),
Stuff::String(String::from("a")),
Stuff::Point(Point::default()),
];
// vec = [0, "a", Point { x: 0, y: 0 }]
vec = vec.into_iter().map(map_stuff).collect();
// vec = [1, "a!", Point { x: 1, y: 1 }]
}
尽管 Any
特性鲜少是必须要被使用的,但有时它又是一种非常便捷的用法,我们将在 错误处理 一章中领会这一点。
文本格式化特性
我们可以使用 std::fmt
中提供的文本格式化宏来序列化结构体,例如我们最熟悉的 println!
。我们可以将文本格式化的参数传入 {}
占位符,以选择具体用哪个特性来序列化该结构。
特性 | 占位符 | 描述 |
---|---|---|
Display | {} | 常规序列化 |
Debug | {:?} | 调试序列化 |
Octal | {:o} | 八进制序列化 |
LowerHex | {:x} | 小写十六进制序列化 |
UpperHex | {:X} | 大写十六进制序列化 |
Pointer | {:p} | 内存地址 |
Binary | {:b} | 二进制序列化 |
LowerExp | {:e} | 小写指数序列化 |
UpperExp | {:E} | 大写十六进制序列化 |
Display & ToString
预备知识
实现 Display
特性的类型可以被序列化为 String
。这对于程序的用户来说非常的友好。例如:
use std::fmt;
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
println!("origin: {}", Point::default());
// prints "origin: (0, 0)"
// get Point's Display representation as a String
// Point 表达为可显示的 String
let stringified_point = format!("{}", Point::default());
assert_eq!("(0, 0)", stringified_point); // ✅
}
除了使用 format!
宏来序列化结构体,我们也可以使用 ToString
特性:
我们不需要自己手动实现,事实上,我们也不能,因为对于实现了 Display
的类型来说,ToString
是由通用泛型实现所自动实现的。
Using
ToString
withPoint
:
对 Point
使用 ToString
特性:
Debug
预备知识
Debug
与 Display
具有相同的签名。唯一的区别在于我们使用 {:?}
文本格式化指令来调用 Debug
特性。 Debug
特性可以使用如下方法衍生:
为特定类型实现 Debug
特性的同时,这也使得我们可以使用 dbg!
宏来快速地调试程序,这种方式要优于 println!
。其优点在于:
dbg!
输出到标准错误流而不是标准输出流,所以我们能够很容易地将调试信息提取出来。dbg!
同时输出值和值的求值表达式。dbg!
接管参数的属权,但不会吞掉参数,而是再抛出来,所以可以将它用在表达式中:
dbg!
宏唯一的缺点是,它不能在构建最终发布的二进制文件时自动删除,我们不得不手动删除相关代码。
算符重载特性
在 Rust 中,所有的算符都与相应的特性相关联。为特定类型实现相应特性,即为该类型实现了相应算符。
特性 | 类别 | 算符 | 描述 |
---|---|---|---|
Eq , PartialEq | 比较 | == | 相等 |
Ord , PartialOrd | 比较 | < , > , <= , >= | 比较 |
Add | 算数 | + | 加 |
AddAssign | 算数 | += | 加等于 |
BitAnd | 算数 | & | 按位与 |
BitAndAssign | 算数 | &= | 按位与等于 |
BitXor | 算数 | ^ | 按位异或 |
BitXorAssign | 算数 | ^= | 按位异或等于 |
Div | 算数 | / | 除 |
DivAssign | 算数 | /= | 除等于 |
Mul | 算数 | * | 乘 |
MulAssign | 算数 | *= | 乘等于 |
Neg | 算数 | - | 一元负 |
Not | 算数 | ! | 一元逻辑非 |
Rem | 算数 | % | 求余 |
RemAssign | 算数 | %= | 求余等于 |
Shl | 算数 | << | 左移 |
ShlAssign | 算数 | <<= | 左移等于 |
Shr | 算数 | >> | 右移 |
ShrAssign | 算数 | >>= | 右移等于 |
Sub | 算数 | - | 减 |
SubAssign | 算数 | -= | 减等于 |
Fn | 闭包 | (...args) | 不可变闭包调用 |
FnMut | 闭包 | (...args) | 可变闭包调用 |
FnOnce | 闭包 | (...args) | 一次性闭包调用 |
Deref | 其它 | * | 不可变解引用 |
DerefMut | 其它 | * | 可变解引用 |
Drop | 其它 | - | 类型析构 |
Index | 其它 | [] | 不可变索引 |
IndexMut | 其它 | [] | 可变索引 |
RangeBounds | 其它 | .. | 范围迭代 |
比较特性 Comparison Traits
特性 | 类别 | 算符 | 描述 |
---|---|---|---|
Eq , PartialEq | 比较 | == | 相等 |
Ord , PartialOrd | 比较 | < , > , <= , >= | 比较 |
PartialEq & Eq
预备知识
- Self
- Methods
- Generic Parameters
- Default Impls
- Generic Blanket Impls
- Marker Traits
- Subtraits & Supertraits
- Sized
实现了 PartialEq<Rhs>
特性的类型可以使用 ==
算符来检查与 Rhs
的相等性。
对 PartialEq<Rhs>
的实现须确保实现对称性与传递性。这意味着对于任意 a
, b
和 c
有:
- 若
a == b
则b == a
(对称性) - 若
a == b && b == c
则a == c
(传递性)
默认情况下 Rhs = Self
是因为我们几乎总是在相同类型之间进行比较。这也自动地确保了我们的实现是对称的、可传递的。
如果特定类型的成员都实现了 PartialEq
特性,那么该类型也可衍生该特性:
多亏了通用泛型实现,一旦我们为特定类型实现了 PartialEq
特性,那么直接使用该类型的引用互相比较也是可以的:
由于该特性提供泛型,我们可以定义不同类型之间的可相等性。标准库正是利用这一点提供了不同类型字符串之间的比较功能,例如String
, &str
, PathBuf
,&Path
,OsString
和 &OsStr
等等。
通常来说我们仅会实现相同类型之间的可相等性,除非两种类型虽然包含同一类数据,但又有表达形式或交互形式的差异,这时我们才会考虑实现不同类型之间的可相等性。
以下是一个有趣但糟糕的例子,它尝试为不同类型实现 PartialEq
但又违背了上述要求:
#[derive(PartialEq)]
enum Suit {
Spade,
Club,
Heart,
Diamond,
}
#[derive(PartialEq)]
enum Rank {
Ace,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
}
#[derive(PartialEq)]
struct Card {
suit: Suit,
rank: Rank,
}
// check equality of Card's suit
// 检查花色的相等性
impl PartialEq<Suit> for Card {
fn eq(&self, other: &Suit) -> bool {
self.suit == *other
}
}
// check equality of Card's rank
// 检查牌序的相等性
impl PartialEq<Rank> for Card {
fn eq(&self, other: &Rank) -> bool {
self.rank == *other
}
}
fn main() {
let AceOfSpades = Card {
suit: Suit::Spade,
rank: Rank::Ace,
};
assert!(AceOfSpades == Suit::Spade); // ✅
assert!(AceOfSpades == Rank::Ace); // ✅
}
上述代码有效且其逻辑有几分道理,黑桃 A 既是黑桃也是 A 。但如果我们真的去写一个处理扑克牌的库的话,最简单也最方便的方法莫过于独立地检查牌面的花色和牌序。而且,上述代码并不满足对称性!我们可以使用 Card == Suit
和 Card == Rank
,但却不能使用 Suit == Card
和 Rank == Card
, 让我们来修复这一点:
我们实现了对称性!棒!但是实现对称性却破坏了传递性!糟tm大糕!考虑以下代码:
fn main() {
// Ace of Spades
// ♠A
let a = Card {
suit: Suit::Spade,
rank: Rank::Ace,
};
let b = Suit::Spade;
// King of Spades
// ♠K
let c = Card {
suit: Suit::Spade,
rank: Rank::King,
};
assert!(a == b && b == c); // ✅
assert!(a == c); // ❌
}
关于对不同类型实现 PartialEq
特性的绝佳示例如下,本程序的功能在于处理空间上的距离,它使用不同的类型以表示不同的测量单位:
#[derive(PartialEq)]
struct Foot(u32);
#[derive(PartialEq)]
struct Yard(u32);
#[derive(PartialEq)]
struct Mile(u32);
impl PartialEq<Mile> for Foot {
fn eq(&self, other: &Mile) -> bool {
self.0 == other.0 * 5280
}
}
impl PartialEq<Foot> for Mile {
fn eq(&self, other: &Foot) -> bool {
self.0 * 5280 == other.0
}
}
impl PartialEq<Mile> for Yard {
fn eq(&self, other: &Mile) -> bool {
self.0 == other.0 * 1760
}
}
impl PartialEq<Yard> for Mile {
fn eq(&self, other: &Yard) -> bool {
self.0 * 1760 == other.0
}
}
impl PartialEq<Foot> for Yard {
fn eq(&self, other: &Foot) -> bool {
self.0 * 3 == other.0
}
}
impl PartialEq<Yard> for Foot {
fn eq(&self, other: &Yard) -> bool {
self.0 == other.0 * 3
}
}
fn main() {
let a = Foot(5280);
let b = Yard(1760);
let c = Mile(1);
// symmetry
// 对称性
assert!(a == b && b == a); // ✅
assert!(b == c && c == b); // ✅
assert!(a == c && c == a); // ✅
// transitivity
// 传递性
assert!(a == b && b == c && a == c); // ✅
assert!(c == b && b == a && c == a); // ✅
}
Eq
是仅用于标记的特性,也是 PartialEq<Self>
的子特性。
鉴于 PartialEq
特性提供的对称性与传递性,一旦我们实现 Eq
特性,我们也就确保了该类型具有自反性,即对任意 a
有 a == a
。可以说, Eq
改良了 PartialEq
,因为它实现了一个比后者更加严格的可相等性。如果一个类型的全部成员都实现了 Eq
特性,那么该类型本身也可以衍生出该特性。
所有的浮点类型都实现了 PartialEq
但是没有实现 Eq
,因为 NaN != NaN
。几乎所有其它实现 PartialEq
的类型也都自然地实现了 Eq
,除非它们包含了浮点数。
对于实现了 PartialEq
和 Debug
的类型,我们也可以将它用于 assert_eq!
宏。并且,我们可以对实现 PartialEq
特性的类型组成的集合进行比较。
Hash
预备知识
本特性并未关联到任何算符,之所以在这里提及,是因为它与 PartialEq
与 Eq
密切的关系。实现 Hash
特性的类型可以通过 Hasher
作哈希运算。
以下衍生宏展开与以上代码中相同的实现:
如果一个类型同时实现了 Hash
和 Eq
,那么二者必须要实现步调一致,即对任意 a
与 b
, 若有 a == b
, 则必有 a.hash() == b.hash()
。所以,对于同时实现二者,要么都用衍生宏,要么都手动实现,不要一个用衍生宏,而另一个手动实现,否则我们将冒着步调不一致的极大风险。
实现Eq
和 Hash
特性的主要好处在于,这允许我们将该类型作为一个键存储于 HashMap
和 HashSet
中。
PartialOrd & Ord
预备知识
- Self
- Methods
- Generic Parameters
- Default Impls
- Subtraits & Supertraits
- Derive Macros
- Sized
- PartialEq & Eq
实现 PartialOrd<Rhs>
的类型可以和 Rhs
的类型之间使用 <
,<=
,>
,和 >=
算符。
实现 PartialOrd
时须确保比较的非对称性和传递性。这意味着对任意 a
,b
,c
有:
- 若
a < b
则!(a > b)
(非对称性) - 若
a < b && b < c
则a < c
(传递性)
PartialOrd
是 PartialEq
的子特性,二者必须要实现步调一致。
PartialOrd
改良了 PartialEq
,后者仅能比较是否相等,而前者除了能比较是否相等,还能比较孰大孰小。
默认情况下 Rhs = Self
,因为我们几乎总是在相同类型的实例之间相比较,而不是不同类型之间。这一点自动保证了我们的实现的对称性和传递性。
如果特定类型的全部成员都实现了 PartialOrd
特性,那么该类型也可以衍生出该特性:
PartialOrd
衍生宏依据 类型成员的定义顺序 对类型进行排序:
Ord
是 Eq
和 PartialOrd<Self>
的子特性:
鉴于 PartialOrd
提供的非对称性和传递性,对特定类型实现 Ord
特性的同时也就保证了其非对称性,即对于任意 a
与 b
有 a < b
,a == b
,a < b
。可以说, Ord
改良了 Eq
和 PartialOrd
,因为它提供了一种更加严格的比较。如果一个类型实现了 Ord
,那么 PartialOrd
,PartialEq
和 Eq
的实现也就微不足道了。
浮点数类型实现了 PartialOrd
但是没有实现 Ord
,因为 NaN < 0 == false
与 NaN >= 0 == false
同时为真。几乎所有其它实现 PartialOrd
的类型都实现了 Ord
,除非该类型包含浮点数。
对于实现了 Ord
特性的类型,我们可以将它存储于 BTreeMap
和 BTreeSet
,并且可以通过 sort()
方法对切片,或者任何可以自动解引用为切片的类型进行排序,例如 Vec
和 VecDeque
。
算术特性 Arithmetic Traits
特性 | 类别 | 算符 | 描述 |
---|---|---|---|
Add | 算数 | + | 加 |
AddAssign | 算数 | += | 加等于 |
BitAnd | 算数 | & | 按位与 |
BitAndAssign | 算数 | &= | 按位与等于 |
BitXor | 算数 | ^ | 按位异或 |
BitXorAssign | 算数 | ^= | 按位异或等于 |
Div | 算数 | / | 除 |
DivAssign | 算数 | /= | 除等于 |
Mul | 算数 | * | 乘 |
MulAssign | 算数 | *= | 乘等于 |
Neg | 算数 | - | 一元负 |
Not | 算数 | ! | 一元逻辑非 |
Rem | 算数 | % | 求余 |
RemAssign | 算数 | %= | 求余等于 |
Shl | 算数 | << | 左移 |
ShlAssign | 算数 | <<= | 左移等于 |
Shr | 算数 | >> | 右移 |
ShrAssign | 算数 | >>= | 右移等于 |
Sub | 算数 | - | 减 |
SubAssign | 算数 | -= | 减等于 |
详解以上所有算术特性未免显得多余,且其大多仅用于操作数字类型。本文仅就最常见被重载的 Add
和 AddAssign
特性,亦即 +
和 +=
算符,进行说明,其重载广泛用于为集合增加内容或对不同事物的连接。这样,我们多侧重于最有趣的地方,而不是无趣枯燥地重复。
Add & AddAssign
预备知识
实现 Add<Rhs, Output = T>
特性的类型,与 Rhs
类型相加得到 T
类型的值。
下例对 Point
类型实现了 Add<Rhs, Output = T>
:
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2;
assert_eq!(p3.x, p1.x + p2.x); // ✅
assert_eq!(p3.y, p1.y + p2.y); // ✅
}
如果我们对 Point
的引用进行如上操作还能将他们加在一起吗?我们试试:
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = &p1 + &p2; // ❌
}
遗憾的是,并不可以。编译器出错了:
error[E0369]: cannot add `&Point` to `&Point`
--> src/main.rs:50:25
|
50 | let p3: Point = &p1 + &p2;
| --- ^ --- &Point
| |
| &Point
|
= note: an implementation of `std::ops::Add` might be missing for `&Point`
在 Rust 的类型系统中,对于特定类型 T
来讲,T
,&T
,&mut T
三者本身是具有不同类型的,这意味着我们需要对它们分别实现相应特性。下面我们对 &Point
实现 Add
特性:
impl Add for &Point {
type Output = Point;
fn add(self, rhs: &Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = &p1 + &p2; // ✅
assert_eq!(p3.x, p1.x + p2.x); // ✅
assert_eq!(p3.y, p1.y + p2.y); // ✅
}
这是可行的,但是不觉得哪里怪怪的吗?我们对 Point
和 &Point
分别实现了 Add
特性,现在来看这两种实现能够保持步调一致,但是未来也能保证吗?例如,我们现在决定对两个 Point
相加要产生一个 Line
而不是 Point
,可以对 Add
特性的实现做出如下改动:
use std::ops::Add;
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
#[derive(Copy, Clone)]
struct Line {
start: Point,
end: Point,
}
// we updated this impl
// 我们更新了这个实现
impl Add for Point {
type Output = Line;
fn add(self, rhs: Point) -> Line {
Line {
start: self,
end: rhs,
}
}
}
// but forgot to update this impl, uh oh!
// 但是忘记了更新这个实现,糟tm大糕!
impl Add for &Point {
type Output = Point;
fn add(self, rhs: &Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = p1 + p2; // ✅
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = &p1 + &p2; // ❌ expected Line, found Point
// ❌ 期待得到 Line ,但是得到 Point
}
我们对 &Point
不可变引用类型的 Add
实现,给我们带来了不必要的维护困难。是否能够使得,当我们更改 Point
类型的实现时, &Point
类型的实现也能够自动发生匹配,而不需要我们手动维护呢?我们的愿望是尽可能写出 DRY (Don't Repeat Yourself)
的不重复的代码。幸运的是,我们可以如此实现这一点:
// updated, DRY impl
// 使用一种更“干”的实现
impl Add for &Point {
type Output = <Point as Add>::Output;
fn add(self, rhs: &Point) -> Self::Output {
Point::add(*self, *rhs)
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = p1 + p2; // ✅
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = &p1 + &p2; // ✅
}
实现 AddAssign<Rhs>
的类型,允许我们对 Rhs
的类型相加之并赋值到自身。该特性的声明为:
对 Point
和 &Point
类型的实现示例:
use std::ops::AddAssign;
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32
}
impl AddAssign for Point {
fn add_assign(&mut self, rhs: Point) {
self.x += rhs.x;
self.y += rhs.y;
}
}
impl AddAssign<&Point> for Point {
fn add_assign(&mut self, rhs: &Point) {
Point::add_assign(self, *rhs);
}
}
fn main() {
let mut p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
p1 += &p2;
p1 += p2;
assert!(p1.x == 7 && p1.y == 10);
}
闭包特性 Closure Traits
特性 | 类别 | 算符 | 描述 |
---|---|---|---|
Fn | 闭包 | (...args) | 不可变闭包调用 |
FnMut | 闭包 | (...args) | 可变闭包调用 |
FnOnce | 闭包 | (...args) | 一次性闭包调用 |
FnOnce, FnMut, & Fn
预备知识
- Self
- Methods
- Associated Types
- Generic Parameters
- Generic Types vs Associated Types
- Subtraits & Supertraits
事实上,在 stable Rust 中我们并不能对我们自己的类型实现上述特性,唯一的例外是闭包。对于闭包从环境中捕获的值的不同,该闭包会实现不同的特性:FnOnce
,FnMut
,Fn
。
对于实现 FnOnce
的闭包,仅可调用一次,因为它消耗掉了其执行中必须的值:
fn main() {
let range = 0..10;
let get_range_count = || range.count();
assert_eq!(get_range_count(), 10); // ✅
get_range_count(); // ❌
}
迭代器上的 .count()
方法会消耗掉整个迭代器,所以该方法仅能调用一次。所以我们的闭包也就是能调用一次了,这就是为什么当第二次调用该闭包时会出错:
error[E0382]: use of moved value: `get_range_count`
--> src/main.rs:5:5
|
4 | assert_eq!(get_range_count(), 10);
| ----------------- `get_range_count` moved due to this call
5 | get_range_count();
| ^^^^^^^^^^^^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `range` out of its environment
--> src/main.rs:3:30
|
3 | let get_range_count = || range.count();
| ^^^^^
note: this value implements `FnOnce`, which causes it to be moved when called
--> src/main.rs:4:16
|
4 | assert_eq!(get_range_count(), 10);
| ^^^^^^^^^^^^^^^
对于实现 FnMut
特性的闭包,我们可以多次调用,且其可以改变其从环境捕获的值。我们可以说实现 FnMut
的闭包的执行具有副作用,或者说它是具有状态的。下例展示了一个闭包,它通过跟踪最小值,来找到一个迭代器中所有非升序的值:
fn main() {
let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
let mut min = i32::MIN;
let ascending = nums.into_iter().filter(|&n| {
if n <= min {
false
} else {
min = n;
true
}
}).collect::<Vec<_>>();
assert_eq!(vec![0, 4, 8, 10, 15, 18], ascending); // ✅
}
FnMut
改良了 FnOnce
,FnOnce
需要接管参数的属权因此只能调用一次,而 FnMut
只需要参数的可变引用即可并可调用多次。FnMut
可以在所有 FnOnce
可用的地方使用。
对于实现 Fn
特性的闭包,我们可以调用多次,且其不改变任何从环境中捕获的变量。我们可以说实现 Fn
的闭包的执行不具有副作用,或者说它是不具有状态的。下例展示了一个闭包,它通过与栈上的值进行比较,过滤掉一个迭代器中所有比它小的值:
fn main() {
let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
let min = 9;
let greater_than_9 = nums.into_iter().filter(|&n| n > min).collect::<Vec<_>>();
assert_eq!(vec![10, 15, 18, 13], greater_than_9); // ✅
}
Fn
改良了 FnMut
,尽管它们都可以多次调用,但是 FnMut
需要参数的可变引用,而 Fn
仅需要参数的不可变引用。Fn
可以在所有 FnMut
和 FnOnce
可用的地方使用。
如果一个闭包不从环境中捕获任何的值,那么从技术上讲它就不是闭包,而仅仅只是一个内联的匿名函数。并且它可以被转换为、用于或传递为一个常规函数指针,即 fn
。函数指针可以用于任何 Fn
,FnMut
,FnOnce
可用的地方。
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let mut fn_ptr: fn(i32) -> i32 = add_one;
assert_eq!(fn_ptr(1), 2); // ✅
// capture-less closure cast to fn pointer
// 不捕获环境的闭包可转换为普通函数指针
fn_ptr = |x| x + 1; // same as add_one
assert_eq!(fn_ptr(1), 2); // ✅
}
以下示例中,将常规函数作为闭包而传入:
fn main() {
let nums = vec![-1, 1, -2, 2, -3, 3];
let absolutes: Vec<i32> = nums.into_iter().map(i32::abs).collect();
assert_eq!(vec![1, 1, 2, 2, 3, 3], absolutes); // ✅
}
其它特性 Other Traits
特性 | 类别 | 算符 | 描述 |
---|---|---|---|
Deref | 其它 | * | 不可变解引用 |
DerefMut | 其它 | * | 可变解引用 |
Drop | 其它 | - | 类型析构 |
Index | 其它 | [] | 不可变索引 |
IndexMut | 其它 | [] | 可变索引 |
RangeBounds | 其它 | .. | 范围迭代 |
Deref & DerefMut
预备知识
实现 Deref<Target = T>
的类型,可以通过 *
解引用算符,解引用到 T
类型。智能指针是该特性的著名实现者,例如 Box
和 Rc
。不过,我们很少在 Rust 编程中看到解引用算符,这是由于 Rust 的强制解引用的特性所导致的。
当作为函数的参数、函数的返回值、方法的调用参数时,Rust 会自动地解引用。这就是为什么我们可以将 &String
或 &Vec<T>
类型的值作为参数传递给接受 str
或 &[T]
类型的参数的函数,因为 String
实现了 Deref<Target = str>
,Vec<t>
实现了 Deref<Target = [T]>
。
Deref
和 DerefMut
仅应实现于智能指针类型。最常见的误用或滥用就是,人们经常希望强行把某种面向对象编程风格的数据继承塞到 Rust 编程中。这是不可能的,因为 Rust 不是面向对象的。让我们用一个例子来领会到底为什么这是不可以的:
事实上,并不可以这么做。首先,强制解引用仅用于引用,所以我们不能移交属权:
其次,强制解引用不可用于泛型编程。例如某特性仅对人类实现:
强制解引用可以用于许多情况,但绝不是所有情况。例如对于算符的操作数而言就不行,即便算符仅是一种方法调用的语法糖。比如,我们希望使用 +=
算符来表达法师学习咒语。
在带有面向对象风格的数据继承的语言中,方法中的 self
值的类型总是等同于调用该方法的类型。但是在 Rust 语言中,self
值的类型总是等同于实现该方法时的类型。
上述特性常令人感到困惑,特别是在对新类型实现 Deref
和 DerefMut
的时候。例如我们想要设计一个 SortedVec
类型,相比于 Vec
类型,它总是处于已排序的状态。我们可能会这样做:
显然我们不能为其实现 DerefMut<Target = Vec<T>>
,因为这可能会破坏排序状态。实现 Deref<Target = Vec<T>>
必须要保证功能的正确性。尝试指出下列代码中的 bug :
use std::ops::Deref;
struct SortedVec<T: Ord>(Vec<T>);
impl<T: Ord> SortedVec<T> {
fn new(mut vec: Vec<T>) -> Self {
vec.sort();
SortedVec(vec)
}
fn push(&mut self, t: T) {
self.0.push(t);
self.0.sort();
}
}
impl<T: Ord> Deref for SortedVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Vec<T> {
&self.0
}
}
fn main() {
let sorted = SortedVec::new(vec![2, 8, 6, 3]);
sorted.push(1);
let sortedClone = sorted.clone();
sortedClone.push(4);
}
鉴于我们从未对 SortedVec
实现 Clone
特性,所以当我们调用 .clone()
方法的时候,编译器会使用强制解引用将该方法调用解析为 Vec
的方法调用,所以该方法返回的是 Vec
而不是 SortedVec
!
fn main() {
let sorted: SortedVec<i32> = SortedVec::new(vec![2, 8, 6, 3]);
sorted.push(1); // still sorted
// calling clone on SortedVec actually returns a Vec 🤦
let sortedClone: Vec<i32> = sorted.clone();
sortedClone.push(4); // sortedClone no longer sorted 💀
}
切记,Rust 并非设计为面向对象的语言,也并不将面向对象编程的模式作为一等公民,所以以上的限制、约束和令人困惑的特性并不被认为是在语言中是错误的。
本节的主旨即是使读者领会为什么不要自作聪明地实现 Deref
和 DerefMut
特性。这类特性确仅适合于智能指针类的类型,目前来讲标准库中的智能指针的实现,确需要这样的不稳定特性以及一些编译器魔法才能工作。如果我们确需要一些类似于Deref
和 DerefMut
的特性,不妨使用 AsRef
和 AsMut
特性。我们将在后面的章节中对这类特性做出说明。
Index & IndexMut
预备知识
- Self
- Methods
- Associated Types
- Generic Parameters
- Generic Types vs Associated Types
- Subtraits & Supertraits
- Sized
对于实现 Index<T, Output = U>
的类型,我们可以使用 []
索引算符对 T
类型的值索引 &U
类型的值。作为语法糖,编译器也会为索引操作返回的值自动添加一个 *
解引用算符。
fn main() {
// Vec<i32> impls Index<usize, Output = i32> so
// indexing Vec<i32> should produce &i32s and yet...
// 鉴于 Vec<i32> 实现了 Index<usize, Output = i32>
// 所以对 Vec<i32> 的索引应当返回 &i32 类型的值,但是。。。
let vec = vec![1, 2, 3, 4, 5];
let num_ref: &i32 = vec[0]; // ❌ expected &i32 found i32
// above line actually desugars to
// 以上代码等价于
let num_ref: &i32 = *vec[0]; // ❌ expected &i32 found i32
// both of these alternatives work
// 以下是建议使用的一对形式
let num: i32 = vec[0]; // ✅
let num_ref: &i32 = &vec[0]; // ✅
}
令人困惑的是,似乎 Index
特性没有遵循它自己的方法签名,但其实真正有问题的是语法糖。
鉴于 Idx
是泛型类型,Index
特性对多个给定类型可以多次实现。并且对于 Vec<T>
,我们不仅可以对 usize
索引,还可以对 Range<usize>
索引得到切片。
fn main() {
let vec = vec![1, 2, 3, 4, 5];
assert_eq!(&vec[..], &[1, 2, 3, 4, 5]); // ✅
assert_eq!(&vec[1..], &[2, 3, 4, 5]); // ✅
assert_eq!(&vec[..4], &[1, 2, 3, 4]); // ✅
assert_eq!(&vec[1..4], &[2, 3, 4]); // ✅
}
为了展示如何自己实现 Index
特性,以下是一个有趣的例子,它设计了一个 Vec
的包装结构,其使得循环索引和负数索引成为可能:
Idx
的类型并不非得是数字类型或 Range
类型,甚至还可以是枚举!例如我们可以在一支篮球队中,对打什么位置索引从而得到队伍里打这个位置的队员:
Drop
预备知识
对于实现 Drop
特性的类型,在该类型脱离作用域并销毁前,其 drop
方法会被调用。通常,不必为我们的类型实现这一特性,除非该类型持有某种外部的资源,且该资源需要显式释放。
标准库中的 BufWriter
类型允许我们对向 Write
类型写入的时候进行缓存。显然,当 BufWriter
销毁前应当把缓存的内容写入 Writer
实例,这就是 Drop
所允许我们做到的!对于实现了 Drop
的 BufWriter
来说,其实例在销毁前会总会调用 flush
方法。
并且,在 Rust 中 Mutex
类型之所以没有 unlock()
方法,就是因为它完全不需要!鉴于 Drop
特性的实现,调用 Mutex
的 lock()
方法返回的 MutexGuard
类型,在脱离作用域时会自动地释放 Mutex
。
简而言之,如果你正在设计某种需要显示释放的资源的抽象包装,那么这正是 Drop
特性大显神威的地方。
转换特性
From & Into
预备知识
实现 From<T>
特性的类型允许我们从 T
类型转换到自身的类型 Self
。
实现 Into<T>
特性的类型允许我们从自身的类型 Self
转换到 T
类型。
这是一对恰好相反的特性,如同一枚硬币的两面。注意,我们只能手动实现 From<T>
特性,而不能手动实现 Into<T>
特性,因为 Into<T>
特性已经被通用泛型实现所自动实现。
这两个特性同时存在的一个好处在于,我们可以在为泛型类型添加约束的时候,使用两种稍有不同的记号:
对于具体使用哪种记号并无一定之规,请根据实际情况做出最恰当的选择。接下来我们看看 Point
类型的例子:
这样的转换并不是对称的,如果我们想将 Point
转换为元组或数组,那么我们需要显式地编写相应的代码:
借由 From<T>
特性,我们可以省却大量编写模板代码的麻烦。例如,我们现在具有一个包含三个 Point
的类型 Triangle
类型,以下是构造该类型的几种办法:
对于 From<T>
特性的使用并无一定之规,运用你的智慧明智地使用它吧!
使用 Into<T>
特性的一个神奇之处在于,对于那些本来只能接受特定类型参数的函数,现在你可以有更多不同的选择:
错误处理 Error Handling
讲解错误处理与 Error
特性的最佳时机,莫过于在 Display
, Debug
, Any
和 From
之后, TryFrom
之前,这就是为什么我要将 错误处理 这一节硬塞在 转换特性 这一章里。
Error
预备知识
- Self
- Methods
- Default Impls
- Generic Blanket Impls
- Subtraits & Supertraits
- Trait Objects
- Display & ToString
- Debug
- Any
- From & Into
在 Rust 中,错误是被返回的,而不是被抛出的。让我们看看下面的例子:
由于整数的除零操作会导致 panic ,为了程序的健壮性,我们显式地实现了安全的 safe_div
除法函数,它的返回值是 Result
:
由于错误是被返回的,而不是被抛出的,它们必须被显式地处理。如果当前函数没有处理该错误的能力,那么该错误应当原路返回到上一级调用函数。最理想的返回错误的方法是使用 ?
算符,它是现在已经过时的 try!
宏的语法糖:
例如,如果我们的函数其功能是将文件读为一个 String
,那么使用 ?
算符来将可能的错误 io::Error
返回给上级调用函数就很方便:
又例如,如果我们的文件是一系列数字,我们想将它们加在一起,可以这样编写代码:
现在 Rusult
的类型又如何?该函数内部可能产生 io::Error
或 ParseIntError
两种错误。我们将介绍三种解决此类问题的方法,从最简单但不优雅的方法,到最健壮的方法:
方法一,我们注意到,所有实现了 Error
的类型同时也实现了 Display
,因此我们可以将错误映射到 String
并以此为错误类型:
此方法的明显缺点在于,由于我们将所有的错误都序列化了,以至于丢弃了该错误的类型信息,这对于上级调用函数错误处理来讲,就不是那么方便了。
但此方法也有一个不明显的优点,那就是我们可以使用自定义的字符串,来提供丰富的上下文错误信息。例如,ParseIntError
通常序列化为 "invalid digit found in string"
这样模棱两可的文本,既没有提及无效的字符串是什么,也没有提及它要转换到什么样的数字类型。这样的信息对于我们调试程序来讲几乎没有什么帮助。不过我们可以提供更有意义的,且上下文相关的信息来明显改善这一点:
The second approach takes advantage of this generic blanket impl from the standard library:
方法二,利用标准库的通用泛型实现:
所有实现了 Error
特性的类型都可以隐式地使用 ?
转换为 Box<dyn error::Error>
类型。所以我们可以将 Rusult
的错误类型设为 Box<dyn error::Error>
类型,然后 ?
算符会帮我们实现这一隐式转换。
这看起来似乎有与第一种方法一样的缺点,丢弃了错误的类型信息。有时确实如此,但倘若上级调用函数知悉该函数的实现细节,那么它仍然可以通过 error::Error
特性的 downcast_ref()
方法来分辨错误的具体类型,这与实现了 dyn Any
特性的类型是一样的:
方法三,处理错误的最健壮和类型安全的方法,是通过枚举来构建我们自己的错误类型:
转换特性深入
TryFrom & TryInto
预备知识
- Self
- Functions
- Methods
- Associated Types
- Generic Parameters
- Generic Types vs Associated Types
- Generic Blanket Impls
- From & Into
- Error
TryFrom
和 TryInto
是可能失败版本的 From
和 Into
。
与 Into
相似地,我们不能手动实现 TryInto
,因为它已经为通用泛型实现所提供。
例如,我们的程序要求 Point
的 x
和 y
的值必须要处于 -1000
到 1000
之间,相较于 From
,使用 TryFrom
可以告知上级调用者,某些转换可能失败了。
现在,我们对 Triangle
使用 TryFrom<[TryInto<Point>; 3]>
进行重构:
FromStr
预备知识
实现 FromStr
特性的类型允许可失败地从 &str
转换至 Self
。使用这一特性的理想方式是,调用 &str
实例的 .parse()
方法:
下例为 Point
实现了 FromStr
特性:
FromStr
与 TryFrom<&str>
具有相同的函数签名。先实现哪个特性无关紧要,因为我们可以利用先实现的特性实现后实现的特性。例如,我们假定 Point
类型已经实现了 FromStr
特性,再来实现 TryFrom<&str>
特性:
AsRef & AsMut
预备知识
AsRef
特性的存在很大程度上便捷了引用转换,其最常见的使用是为函数的引用类型的参数的传入提供方便:
另外一个常见的使用是,返回一个包装类型的内部私有数据的引用(该类型用于保证内部私有数据的不变性)。标准库中的 String
就是对 Vec<u8>
的这样一种包装:
之所以不公开内部的 Vec
数据,是因为一旦允许用户随意修改内部数据,就有可能破环 String
有效的 UTF-8 编码。但是,对外开放一个只读的字节数组的引用是安全的,所以有如下实现:
通常来讲我们不对类型实现 AsRef
特性,除非该类型包装了其它类型以提供额外的功能,或是对内部类型提供了不变性的保护。
以下是实现 AsRef
特性的一个反例:
乍看起来这似乎有几分道理,但是当我们对 User
类型添加新的成员时,缺点就暴露出来了:
User
类型由多个 String
和 u32
类型的成员所组成,但我们也不能说 User
是 String
或 u32
吧?即便由更加具体的类型来构造也不行:
对于 User
这样的类型来讲,实现 AsRef
特性并没有什么太多意义。因为 AsRef
的存在仅是为了做一种最简单的引用转换,这种转换最好存在于语义上相类似的事务之间。Name
,Email
,Age
和 Height
其本身和 User
就不是一回事,在逻辑上谈不上转换。
下例展示了 AsRef
特性的正确用法,我们实现了一个新的类型 Moderator
,它仅仅是包装了 User
类型,并对其添加了权限控制:
之所以可以这样做,是因为 Moderator
就是 User
。下例是将 Deref
一节中的例子使用 AsRef
做出替代:
之所以 Deref
在上例之前的版本中不可使用,是因为自动解引用是一种隐式的转换,这就为程序员错误地使用留下了巨大的空间。
而 AsRef
在上例中可以使用,是因为其实现的转换是显式的,这样很大程度上就消除了犯错误的空间。
Borrow & BorrowMut
预备知识
- Self
- Methods
- Generic Parameters
- Subtraits & Supertraits
- Sized
- AsRef & AsMut
- PartialEq & Eq
- Hash
- PartialOrd & Ord
这类特性存在的意义旨在于解决特定领域的问题,例如在 Hashset
,HashMap
,BTreeSet
,BtreeMap
中使用 &str
查询 String
类型的键。
我们可以将 Borrow<T>
和 BorrowMut<T>
视作 AsRef<T>
和 AsMut<T>
的严格版本,其返回的引用 &T
具有与 Self
相同的 Eq
,Hash
和 Ord
的实现。这一点在下例的注释中得到很好的解释:
理解这类特性存在的意义,有助于我们揭开 HashSet
,HashMap
,BTreeSet
和 BTreeMap
中某些方法的实现的神秘面纱。但是在实际应用中,几乎没有什么地方需要我们去实现这样的特性,因为再难找到一个需要我们对一个值再创造一个“借用”版本的类型的场景了。对于某种类型 T
,&T
就能解决 99.9% 的问题了,且 T: Borrow<T>
已经被通用泛型实现对 T
实现了,所以我们无需手动实现它,也无需去实现某种的对 U
有 T: Borrow<U>
了。
ToOwned
预备知识
ToOwned
特性是 Clone
特性的泛型版本。 Clone
特性允许我们由 &T
类型得到 T
类型,而 ToOwned
特性允许我们由 &Borrow
类型得到 Owned
类型,其中 Owned: Borrow<Borrowed>
。
换句话讲,我们不能将 &str
克隆为 String
,将 &Path
克隆为 PathBuf
或将 &OsStr
克隆为 OsString
。鉴于 clone
方法的签名不支持这样跨类型的克隆,这就是 ToOwned
特性存在的意义。
与 Borrow
和 BorrowMut
相同地,理解此类特性存在的意义对我们或有帮助,但是鲜少需要我们手动为自己的类实现该特性。
迭代特性
Iterator
预备知识
实现 Iterator<Item = T>
的类型可以迭代产生 T
类型。注意:并不存在 IteratorMut
类型,因为可以通过在实现 Iterator
特性时指定 Item
关联类型,来选择其返回的是不可变引用、可变引用还是自有值。
Vec<T> 方法 | 返回类型 |
---|---|
.iter() | Iterator<Item = &T> |
.iter_mut() | Iterator<Item = &mut T> |
.into_iter() | Iterator<Item = T> |
对于 Rust 的初学者而言可能有些费解,但是对于中级学习者而言则是顺理成章的一件事是 —— 绝大多数类型并不是自己的迭代器。这意味着,如果某种类型是可迭代的,那么应当实现某种额外的迭代器类型去迭代它,而不是让它自己迭代自己。
出于教学的原因,我们在上例中从头手动实现了一个迭代器。而在这种情况下,最理想的做法是直接调用 Vec
的 iter
方法。
另外,最好了解这个通用泛型实现:
任何迭代器的可变引用也是一个迭代器。了解这样的性质有助于我们理解,为什么可以将迭代器的某些参数为 self
的方法当作具有 &mut self
参数的方法来使用。
举个例子,想象我们有这样一个函数,它处理一个具有三个以上值的迭代器,这个函数首先要取得该迭代器的前三个值并分别地处理他们,然后再依次迭代剩余的值。初学者可能会这样实现该函数:
糟糕,take
方法具有 self
参数,这意味着我们不能在不消耗掉整个迭代器的前提下调用该方法。以下可能是一个初学者的改进:
这是可行的,但是理想的改进方式莫过于:
这真是一个很隐蔽的方法,但是被我们抓到了。
同样,对于什么可以是迭代器,什么不可以是,并无一定之规。实现了 Iterator
特性的就是迭代器。而在标准库中,确有一些具有创造性的用例:
IntoIterator
预备知识
闻弦歌而知雅意,实现 IntoIterator
特性的类型可以被转换为迭代器。当用于 for-in
循环时,将自动调用该类型的 into_iter
方法.
不仅 Vec
实现了 IntoIterator
特性,&Vec
与 &mut Vec
同样如此。因此我们可以相应的对可变与不可变的引用,以及自有值进行迭代。
FromIterator
预备知识
顾叶落而晓秋至,实现 FromIterator
特性的类型可以由迭代器而构造。FromIterator
特性最常见和最理想的使用方法是调用 Iterator
的 collect
方法:
下例展示了如何将 Iterator<Item = char>
迭代器的值收集为 String
:
标准库中的全部集合类型都实现了 IntoIterator
和 FromIterator
特性,所以在它们之间进行转换是很方便的:
输入输出特性 I/O Traits
Read & Write
预备知识
Generic blanket impls worth knowing:
值得关注的通用泛型实现:
对于任何实现了 Read
特性的类型,其可变的引用类型也实现了 Read
特性。Write
也是如此。知晓这一点有助于我们理解为什么,对于具有 self
参数的函数可以如同那些具有 &mut self
参数的函数一般使用。鉴于我们已经在 Iterator
特性一节中做出了相近的说明,对此我不再赘述。
我特别指出的是,在 &[u8]
实现 Read
的同时,Vec<u8>
实现了 Write
,因此我们可以很方便地使用 String
来对我们的文件处理函数进行单元测试,因为它可以轻易地转换到 &[u8]
和转换自 Vec<u8>
。
结语 Conclusion
We learned a lot together! Too much in fact. This is us now:
我们真是学习了太多!太多了!可能这就是我们现在的样子:
该漫画的创作者: The Jenkins Comic
讨论 Discuss
Discuss this article on
可以在如下地点讨论本文
通告 Notifications
Get notified when the next blog post get published by
在如下处得知我下一篇博文的详情
- 订阅我的推特 pretzelhammer 或者
- 订阅这个 repo (点击
Watch
-> 点击Custom
-> 选择Releases
-> 点击Apply
)
更多资料 Further Reading
- Sizedness in Rust
- Common Rust Lifetime Misconceptions
- Learning Rust in 2020
- Learn Assembly with Entirely Too Many Brainfuck Compilers
翻译 Translation
鉴于水平所限,
难免出现翻译错误,
如发现错误还请告知!