第13章 实用特性
# 第13章 实用特性
科学只不过是在大自然的千变万化中——或者更确切地说,在我们千变万化的经验中——寻找统一性的探索。用柯勒律治(Coleridge)的话说,诗歌、绘画和艺术也是同样的探索,即在变化中寻求统一。 ——雅各布·布罗诺夫斯基(Jacob Bronowski)
本章将介绍我们所说的Rust “实用” 特性,这些特性来自标准库,种类繁多,它们对Rust的编程方式有着重要影响。为了编写符合习惯用法的代码,以及为你的库设计出用户认为 “很Rust” 的公共接口,你需要熟悉这些特性。它们大致可分为三类:
- 语言扩展特性:正如我们在上一章介绍的运算符重载特性,让你能够在自己定义的类型上使用Rust的表达式运算符一样,标准库中还有其他一些特性,作为Rust的扩展点,能让你将自己的类型与语言更紧密地集成。这些特性包括
Drop
、Deref
和DerefMut
,以及转换特性From
和Into
。我们将在本章介绍这些特性。 - 标记特性:这些特性主要用于约束泛型类型变量,以表达其他方式无法表达的约束。包括
Sized
和Copy
。 - 公共词汇特性:这些特性没有任何神奇的编译器集成功能;你也可以在自己的代码中定义等效的特性。但它们有一个重要作用,就是为常见问题提供了约定俗成的解决方案。这些特性在库与模块之间的公共接口中特别有价值:通过减少不必要的差异,它们使接口更易于理解,同时也增加了不同库的功能可以直接组合使用的可能性,而无需编写样板代码或自定义粘合代码。这些特性包括
Default
、引用借用特性AsRef
、AsMut
、Borrow
和BorrowMut
;可能失败的转换特性TryFrom
和TryInto
;以及ToOwned
特性,它是Clone
的泛化。
这些特性总结在表13-1中。 表13-1 实用特性总结
特性 | 描述 |
---|---|
Drop | 析构函数。Rust在值被丢弃时自动运行的清理代码。 |
Sized | 标记特性,用于在编译时已知大小固定的类型,与动态大小的类型(如切片)相对。 |
Clone | 支持克隆值的类型。 |
Copy | 标记特性,用于可以通过对包含值的内存进行逐字节复制来实现克隆的类型。 |
Deref 和DerefMut | 智能指针类型的特性。 |
Default | 具有合理 “默认值” 的类型。 |
AsRef 和AsMut | 从一种类型借用另一种类型引用的转换特性。 |
Borrow 和BorrowMut | 与AsRef /AsMut 类似的转换特性,但额外保证一致的哈希、排序和相等性。 |
From 和Into | 将一种类型的值转换为另一种类型的转换特性。 |
TryFrom 和TryInto | 用于可能失败的类型转换的转换特性。 |
ToOwned | 将引用转换为拥有所有权的值的转换特性。 |
标准库中还有其他重要的特性。我们将在第15章介绍Iterator
和IntoIterator
。第16章会介绍用于计算哈希码的Hash
特性。第19章将介绍标记线程安全类型的一对特性Send
和Sync
。
# Drop
当一个值的所有者消失时,我们说Rust丢弃了这个值。丢弃一个值意味着释放该值所拥有的其他任何值、堆存储和系统资源。丢弃值的情况有很多:变量超出作用域时;在表达式语句结束时;当你截断向量,从其末尾移除元素时等等。
在大多数情况下,Rust会自动为你处理值的丢弃。例如,假设你定义了以下类型:
struct Appellation {
name: String,
nicknames: Vec<String>
}
2
3
4
一个Appellation
拥有用于存储字符串内容的堆空间和向量元素的缓冲区。每当一个Appellation
被丢弃时,Rust会负责清理所有这些资源,你无需再编写额外的代码。不过,如果你愿意,可以通过实现std::ops::Drop
特性来自定义Rust丢弃你定义类型的值的方式:
trait Drop {
fn drop(&mut self);
}
2
3
Drop
的实现类似于C++中的析构函数,或者其他语言中的终结器。当一个值被丢弃时,如果它实现了std::ops::Drop
,Rust会在像往常一样继续丢弃其字段或元素所拥有的值之前,调用其drop
方法。这种对drop
的隐式调用是调用该方法的唯一方式;如果你试图显式调用它,Rust会将其标记为错误。
因为Rust在丢弃一个值的字段或元素之前会调用Drop::drop
,所以该方法接收到的值始终是完全初始化的。我们为Appellation
类型实现的Drop
可以充分利用其字段:
impl Drop for Appellation {
fn drop(&mut self) {
print!("Dropping {}", self.name);
if !self.nicknames.is_empty() {
print!(" (AKA {})", self.nicknames.join(", "));
}
println!("");
}
}
2
3
4
5
6
7
8
9
有了这个实现,我们可以编写以下代码:
{
let mut a = Appellation {
name: "Zeus".to_string(),
nicknames: vec!["cloud collector".to_string(), "king of the gods".to_string()]
};
println!("before assignment");
a = Appellation { name: "Hera".to_string(), nicknames: vec![] };
println!("at end of block");
}
2
3
4
5
6
7
8
9
当我们将第二个Appellation
赋值给a
时,第一个会被丢弃;当a
超出作用域时,第二个也会被丢弃。这段代码的输出如下:
before assignment
Dropping Zeus (AKA cloud collector, king of the gods)
at end of block
Dropping Hera
2
3
4
由于我们为Appellation
实现的std::ops::Drop
只是打印一条消息,那么它的内存到底是如何清理的呢?Vec
类型实现了Drop
,它会丢弃每个元素,然后释放它们占用的堆分配缓冲区。String
内部使用Vec<u8>
来存储文本,所以String
本身无需实现Drop
;它让其内部的Vec
来负责释放字符。同样的原理也适用于Appellation
值:当一个Appellation
值被丢弃时,最终是Vec
的Drop
实现负责释放每个字符串的内容,最后释放存储向量元素的缓冲区。至于存储Appellation
值本身的内存,它也有某个所有者,可能是一个局部变量或某个数据结构,由这个所有者负责释放它。
如果一个变量的值被移动到其他地方,导致该变量在超出作用域时未初始化,那么Rust不会尝试丢弃该变量:因为其中没有值可丢弃。
即使一个变量的值是否被移动取决于控制流,这条原则仍然适用。在这种情况下,Rust会用一个不可见的标志来跟踪变量的状态,指示该变量的值是否需要被丢弃:
let p;
{
let q = Appellation { name: "Cardamine hirsuta".to_string(),
nicknames: vec!["shotweed".to_string(), "bittercress".to_string()] };
if complicated_condition() {
p = q;
}
}
println!("Sproing! What was that?");
2
3
4
5
6
7
8
9
根据complicated_condition
返回true
还是false
,p
或q
最终会拥有Appellation
,而另一个则未初始化。Appellation
值的最终归属决定了它是在println!
之前还是之后被丢弃,因为q
在println!
之前超出作用域,而p
在之后。虽然一个值可能会在不同地方移动,但Rust只会丢弃它一次。
通常,除非你定义的类型拥有Rust尚未知晓的资源,否则不需要实现std::ops::Drop
。例如,在Unix系统上,Rust标准库内部使用以下类型来表示操作系统文件描述符:
struct FileDesc {
fd: c_int,
}
2
3
FileDesc
的fd
字段只是程序使用完后应该关闭的文件描述符编号;c_int
是i32
的别名。标准库为FileDesc
实现Drop
如下:
impl Drop for FileDesc {
fn drop(&mut self) {
let _ = unsafe { libc::close(self.fd) };
}
}
2
3
4
5
这里,libc::close
是Rust对C库close
函数的称呼。Rust代码只能在unsafe
块中调用C函数,所以这里使用了unsafe
块。
如果一个类型实现了Drop
,它就不能再实现Copy
特性。如果一个类型是Copy
,这意味着简单的逐字节复制就足以生成该值的独立副本。但通常情况下,对同一数据多次调用相同的drop
方法是错误的。
标准前置模块中包含一个用于丢弃值的函数drop
,但它的定义并没有什么神奇之处:
fn drop<T>(_x: T) {}
换句话说,它按值接收参数,从调用者那里获取所有权,然后不对其进行任何操作。当_x
超出作用域时,Rust会像处理其他任何变量一样丢弃它的值。
# Sized
大小确定(sized)类型指的是其所有值在内存中大小都相同的类型。Rust中几乎所有类型都是大小确定的:每个u64
占用8个字节,每个(f32, f32, f32)
元组占用12个字节。甚至枚举类型也是大小确定的:无论实际使用哪个变体,枚举始终占用足够的空间来容纳其最大的变体。虽然Vec<T>
拥有一个大小可变的堆分配缓冲区,但Vec
值本身是一个指向缓冲区、容量和长度的指针,所以Vec<T>
是一个大小确定的类型。
所有大小确定的类型都实现了std::marker::Sized
特性,该特性没有方法或关联类型。Rust会自动为所有适用的类型实现它,你不能自己实现这个特性。Sized
唯一的用途是作为类型变量的约束:像T: Sized
这样的约束要求T
是一个在编译时已知大小的类型。这类特性被称为标记特性(marker traits),因为Rust语言本身用它们来标记某些具有特定有趣特征的类型。
不过,Rust也有一些大小不确定(unsized)的类型,其值的大小并不都相同。例如,字符串切片类型str
(注意,这里没有&
)是大小不确定的。字符串字面量"diminutive"
和"big"
是指向str
切片的引用,分别占用10个字节和3个字节。如图13-1所示。像[T]
(同样没有&
)这样的数组切片类型也是大小不确定的:像&[u8]
这样的共享引用可以指向任意大小的[u8]
切片。由于str
和[T]
类型表示大小不同的值的集合,所以它们是大小不确定的类型。
图13-1 指向大小不确定值的引用
Rust中另一种常见的大小不确定的类型是dyn
类型,即特性对象的引用目标。正如我们在 “特性对象” 中解释的,特性对象是指向某个实现了给定特性的值的指针。例如,&dyn std::io::Write
和Box<dyn std::io::Write>
类型是指向某个实现了Write
特性的值的指针。该引用目标可能是一个文件、网络套接字,或者是你自己实现了Write
特性的某种类型。由于实现Write
特性的类型集合是开放的,所以dyn Write
作为一种类型是大小不确定的:它的值有不同的大小。
Rust不能将大小不确定的值存储在变量中,也不能将它们作为参数传递。你只能通过像&str
或Box<dyn Write>
这样的指针来处理它们,而这些指针本身是大小确定的。如图13-1所示,指向大小不确定值的指针总是一个胖指针,占用两个机器字长:指向切片的指针还会携带切片的长度,特性对象还会携带一个指向方法实现虚表(vtable)的指针。
特性对象和指向切片的指针有很好的对称性。在这两种情况下,类型都缺少使用它所需的信息:不知道[u8]
的长度,你就无法对其进行索引;不知道Box<dyn Write>
所指向的具体值对应的Write
实现,你就无法调用其方法。在这两种情况下,胖指针都补充了类型所缺少的信息,携带了长度或虚表指针。省略的静态信息被动态信息所取代。
由于大小不确定的类型有诸多限制,大多数泛型类型变量应该限制为大小确定的类型。实际上,这种需求非常常见,以至于在Rust中这是隐式默认的:如果你写struct S<T> { ... }
,Rust会理解为struct S<T: Sized> { ... }
。如果你不想这样约束T
,就必须显式取消这种限制,写成struct S<T: ?Sized> { ... }
。?Sized
语法仅用于这种情况,意思是 “不一定是大小确定的”。例如,如果你写struct S<T: ?Sized> { b: Box<T> }
,那么Rust会允许你使用S<str>
和S<dyn Write>
,此时Box
会变成胖指针;同时也允许使用S<i32>
和S<String>
,此时Box
是普通指针。
尽管有这些限制,大小不确定的类型使Rust的类型系统工作得更加顺畅。阅读标准库文档时,你偶尔会遇到类型变量上的?Sized
约束;这几乎总是意味着给定的类型仅通过指针使用,并且允许相关代码同时处理切片、特性对象和普通值。当一个类型变量有?Sized
约束时,人们通常说它的大小是不确定的:它可能是大小确定的,也可能不是。
除了切片和特性对象,还有一种大小不确定的类型。结构体类型的最后一个字段(但只能是最后一个字段)可以是大小不确定的,这样的结构体本身也是大小不确定的。例如,Rc<T>
引用计数指针在内部被实现为指向私有类型RcBox<T>
的指针,RcBox<T>
将引用计数与T
存储在一起。下面是RcBox
的简化定义:
struct RcBox<T: ?Sized> {
ref_count: usize,
value: T,
}
2
3
4
value
字段是Rc<T>
正在计数引用的T
;Rc<T>
解引用后得到的指针指向这个字段。ref_count
字段保存引用计数。
真正的RcBox
只是标准库的实现细节,不供公开使用。但假设我们使用前面的定义。你可以将这个RcBox
与大小确定的类型一起使用,比如RcBox<String>
,结果是一个大小确定的结构体类型。你也可以将它与大小不确定的类型一起使用,比如RcBox<dyn std::fmt::Display>
(其中Display
是用于可以被println!
和类似宏格式化的类型的特性);RcBox<dyn Display>
是一个大小不确定的结构体类型。
你不能直接构建一个RcBox<dyn Display>
值。相反,你首先需要创建一个普通的、大小确定的RcBox
,其值类型要实现Display
,比如RcBox<String>
。然后,Rust允许你将引用&RcBox<String>
转换为胖引用&RcBox<dyn Display>
:
let boxed_lunch: RcBox<String> = RcBox { ref_count: 1, value: "lunch".to_string() };
use std::fmt::Display;
let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;
2
3
在将值传递给函数时,这种转换会隐式发生,所以你可以将&RcBox<String>
传递给一个期望&RcBox<dyn Display>
的函数:
fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
2
3
4
这将产生以下输出:For your enjoyment: lunch
# Clone
std::clone::Clone
特性用于那些可以创建自身副本的类型。Clone
定义如下:
trait Clone : Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
2
3
4
5
6
clone
方法应该构造一个self
的独立副本并返回。由于这个方法的返回类型是Self
,并且函数不能返回大小不确定的值,所以Clone
特性本身扩展了Sized
特性:这使得实现Clone
特性的Self
类型必须是大小确定的。
克隆一个值通常还需要分配其拥有的任何内容的副本,所以克隆在时间和内存上可能开销很大。例如,克隆一个Vec<String>
不仅要复制向量,还要复制其中的每个String
元素。这就是为什么Rust不会自动克隆值,而是要求你显式调用方法。像Rc<T>
和Arc<T>
这样的引用计数指针类型是例外:克隆它们只是增加引用计数并返回一个新指针。
clone_from
方法将self
修改为source
的副本。clone_from
的默认定义只是克隆source
,然后将其移动到*self
中。这总是可行的,但对于某些类型,有更快的方法可以达到相同的效果。例如,假设s
和t
是String
类型。语句s = t.clone();
必须克隆t
,丢弃s
的旧值,然后将克隆的值移动到s
中;这涉及一次堆分配和一次堆释放。但是,如果原来s
的堆缓冲区有足够的容量来容纳t
的内容,就不需要进行分配或释放操作:你可以直接将t
的文本复制到s
的缓冲区中并调整长度。在泛型代码中,只要可能,你都应该使用clone_from
,以便在有优化实现时利用它们。
如果你的Clone
实现只是对类型的每个字段或元素应用clone
,然后用这些克隆值构造一个新值,并且clone_from
的默认定义就足够了,那么Rust会为你实现这些:只需在类型定义上方加上#[derive(Clone)]
。
标准库中几乎所有适合复制的类型都实现了Clone
。像bool
和i32
这样的基本类型实现了该特性。像String
、Vec<T>
和HashMap
这样的容器类型也实现了。有些类型不适合复制,比如std::sync::Mutex
,这些类型没有实现Clone
。有些类型,比如std::fs::File
,可以复制,但如果操作系统没有必要的资源,复制可能会失败;这些类型没有实现Clone
,因为clone
必须是可靠的。相反,std::fs::File
提供了一个try_clone
方法,它返回一个std::io::Result<File>
,可以报告复制失败的情况。
# Copy
在第4章中,我们解释过,对于大多数类型而言,赋值操作是移动值,而非复制值。移动值能让跟踪它们所拥有的资源变得更为简单。但在 “复制类型:移动的例外情况” 中,我们指出了例外情况:不拥有任何资源的简单类型可以是复制(Copy)类型,在这类类型中,赋值会对源值进行复制,而不是移动值并使源值处于未初始化状态。
当时,我们并未明确说明复制(Copy)究竟是什么,现在可以告诉你:如果一个类型实现了std::marker::Copy
标记特性,那么它就是复制类型,该特性定义如下:
trait Copy : Clone { }
为你自己定义的类型实现这个特性确实很容易:
impl Copy for MyType { }
但由于Copy
是一个对语言有着特殊意义的标记特性,Rust仅允许在只需进行浅字节逐字节复制的情况下,一个类型才能实现Copy
。拥有诸如堆缓冲区或操作系统句柄等其他任何资源的类型,都无法实现Copy
。
任何实现了Drop
特性的类型都不可能是Copy
类型。Rust认为,如果一个类型需要特殊的清理代码,那它必然也需要特殊的复制代码,因此不能成为Copy
类型。
与Clone
特性一样,你可以让Rust为你派生Copy
特性,使用#[derive(Copy)]
。你经常会看到同时派生这两个特性,即#[derive(Copy, Clone)]
。
在将一个类型设为Copy
类型之前,请仔细考虑。尽管这样做会让类型使用起来更方便,但它对类型的实现有着严格的限制。隐式复制也可能会带来较高的开销。我们在 “复制类型:移动的例外情况” 中详细解释了这些因素。
# Deref 和 DerefMut
你可以通过实现std::ops::Deref
和std::ops::DerefMut
特性,来指定像*
和.
这样的解引用运算符在你的类型上的行为方式。诸如Box<T>
和Rc<T>
等指针类型实现了这些特性,以便它们能像Rust的内置指针类型那样工作。例如,如果你有一个Box<Complex>
值b
,那么*b
指的是b
所指向的Complex
值,b.re
指的是其真实分量。如果上下文对引用目标进行赋值或借用可变引用,Rust会使用DerefMut
(“可变解引用” )特性;否则,只读访问就足够了,此时会使用Deref
特性。
这些特性的定义如下:
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut : Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
2
3
4
5
6
7
8
deref
和deref_mut
方法接受一个&Self
引用,并返回一个&Self::Target
引用。Target
应该是Self
包含、拥有或引用的内容:对于Box<Complex>
,Target
类型是Complex
。请注意,DerefMut
扩展了Deref
:如果你能对某个值进行解引用并修改它,那么你肯定也应该能够借用对它的共享引用。由于这些方法返回的引用与&self
具有相同的生命周期,所以只要返回的引用存在,self
就会一直处于被借用状态。
Deref
和DerefMut
特性还有另一个作用。由于deref
接受一个&Self
引用并返回一个&Self::Target
引用,Rust利用这一点自动将前一种类型的引用转换为后一种类型。换句话说,如果插入一个解引用调用能够避免类型不匹配,Rust会为你插入一个。实现DerefMut
则为可变引用启用了相应的转换。这些被称为解引用强制转换(deref coercions):一种类型被 “强制” 表现得像另一种类型。
虽然解引用强制转换的操作你自己也可以显式地写出来,但它们非常方便:
- 如果你有某个
Rc<String>
值r
,并想对它应用String::find
方法,你可以直接写r.find('?')
,而不是(*r).find('?')
:方法调用会隐式借用r
,并且由于Rc<T>
实现了Deref<Target=T>
,&Rc<String>
会强制转换为&String
。 - 你可以对
String
值使用诸如split_at
这样的方法,尽管split_at
是str
切片类型的方法,这是因为String
实现了Deref<Target=str>
。String
无需重新实现str
的所有方法,因为你可以从&String
强制转换得到&str
。 - 如果你有一个字节向量
v
,并且想将它传递给一个期望字节切片&[u8]
的函数,你可以直接将&v
作为参数传递,因为Vec<T>
实现了Deref<Target=[T]>
。
如有必要,Rust会连续应用多个解引用强制转换。例如,利用前面提到的强制转换,你可以直接对Rc<String>
应用split_at
方法,因为&Rc<String>
解引用后得到&String
,&String
再解引用得到&str
,而&str
有split_at
方法。例如,假设你有以下类型:
struct Selector<T> {
/// 此`Selector`中可用的元素。
elements: Vec<T>,
/// `elements`中“当前”元素的索引。`Selector`的行为类似于指向当前元素的指针。
current: usize
}
2
3
4
5
6
为了让Selector
的行为符合文档注释中的描述,你必须为该类型实现Deref
和DerefMut
:
use std::ops::{Deref, DerefMut};
impl<T> Deref for Selector<T> {
type Target = T;
fn deref(&self) -> &T {
&self.elements[self.current]
}
}
impl<T> DerefMut for Selector<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.elements[self.current]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
有了这些实现,你可以像这样使用Selector
:
let mut s = Selector { elements: vec!['x', 'y', 'z'], current: 2 };
// 因为`Selector`实现了`Deref`,我们可以使用`*`运算符来引用其当前元素。
assert_eq!(*s, 'z');
// 通过解引用强制转换,直接在`Selector`上使用`char`的方法,断言`'z'`是字母。
assert!(s.is_alphabetic());
// 通过对`Selector`的引用目标赋值,将`'z'`改为`'w'`。
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);
2
3
4
5
6
7
8
Deref
和DerefMut
特性是为实现智能指针类型(如Box
、Rc
和Arc
),以及那些作为你经常通过引用使用的类型的拥有所有权版本(就像Vec<T>
和String
分别是[T]
和str
的拥有所有权版本)而设计的。你不应仅仅为了让Target
类型的方法自动出现在你的类型上,就像C++中基类的方法在子类中可见那样,而为一个类型实现Deref
和DerefMut
。这并不总是能如你所愿地工作,而且一旦出现问题会令人困惑。
解引用强制转换有一个可能会让人感到困惑的注意事项:Rust应用它们来解决类型冲突,但不会用它们来满足类型变量的约束。例如,以下代码运行正常:
let s = Selector { elements: vec!["good", "bad", "ugly"], current: 2 };
fn show_it(thing: &str) { println!("{}", thing); }
show_it(&s);
2
3
在show_it(&s)
这个调用中,Rust看到参数类型为&Selector<&str>
,而函数参数类型为&str
,它会找到Deref<Target=str>
的实现,并根据需要将调用改写为show_it(s.deref())
。
然而,如果你将show_it
改为一个泛型函数,Rust就突然不配合了:
use std::fmt::Display;
fn show_it_generic<T: Display>(thing: T) { println!("{}", thing); }
show_it_generic(&s);
2
3
Rust会报错:
error: `Selector<&str>` doesn't implement `std::fmt::Display`
|
33 | fn show_it_generic<T: Display>(thing: T) { println!("
{}", thing); }
| ------- required by this bound in `show_it_generic`
34 | show_it_generic(&s);
| ^^ `Selector<&str>` cannot be formatted with the
| &*s
2
3
4
5
6
7
8
这可能会让人感到困惑:为什么将函数泛型化会引入错误呢?确实,Selector<&str>
本身没有实现Display
,但它解引用后得到的&str
肯定实现了。
由于你传递的参数类型是&Selector<&str>
,而函数的参数类型是&T
,所以类型变量T
必须是Selector<&str>
。然后,Rust会检查T: Display
这个约束是否满足:由于它不会应用解引用强制转换来满足类型变量的约束,所以这个检查失败了。
为了解决这个问题,你可以使用as
运算符明确写出强制转换:
show_it_generic(&s as &str);
或者,按照编译器的建议,使用&*
来强制进行转换:
show_it_generic(&*s);
# Default
有些类型具有相当明显的默认值:默认的向量或字符串为空,默认的数字是零,默认的Option
是None
等等。这类类型可以实现std::default::Default
特性:
trait Default {
fn default() -> Self;
}
2
3
default
方法只是返回一个新的Self
类型的值。String
对Default
的实现很直观:
impl Default for String {
fn default() -> String {
String::new()
}
}
2
3
4
5
Rust的所有集合类型,如Vec
、HashMap
、BinaryHeap
等等,都实现了Default
,其default
方法返回一个空集合。当你需要构建一个值的集合,但又想让调用者决定具体构建哪种集合时,这会很有帮助。例如,Iterator
特性的partition
方法会使用一个闭包来决定迭代器生成的值的去向,将这些值分割到两个集合中:
use std::collections::HashSet;
let squares = [4, 9, 16, 25, 36, 49, 64];
let (powers_of_two, impure): (HashSet<i32>, HashSet<i32>) = squares.iter().partition(|&n| n & (n - 1) == 0);
assert_eq!(powers_of_two.len(), 3);
assert_eq!(impure.len(), 4);
2
3
4
5
6
闭包|&n| n & (n - 1) == 0
通过位运算来识别2的幂次方数,partition
方法利用这个闭包生成了两个HashSet
。当然,partition
并不局限于HashSet
;只要集合类型实现了Default
(用于生成一个空集合作为起始)和Extend<T>
(用于向集合中添加一个T
类型的值),你就可以用它生成任何你想要的集合。String
实现了Default
和Extend<char>
,所以你可以这样写:
let (upper, lower): (String, String) = "Great Teacher Onizuka".chars().partition(|&c| c.is_uppercase());
assert_eq!(upper, "GTO");
assert_eq!(lower, "reat eacher nizuka");
2
3
Default
的另一个常见用途是为表示大量参数的结构体生成默认值,其中大多数参数你通常不需要更改。例如,glium
库为强大而复杂的OpenGL图形库提供了Rust绑定。glium::DrawParameters
结构体包含24个字段,每个字段控制OpenGL渲染图形的不同细节。glium
的draw
函数期望一个DrawParameters
结构体作为参数。由于DrawParameters
实现了Default
,你可以创建一个并传递给draw
函数,只需要指定你想要更改的字段:
let params = glium::DrawParameters {
line_width: Some(0.02),
point_size: Some(0.02),
..Default::default()
};
target.draw(..., ¶ms).unwrap();
2
3
4
5
6
这会调用Default::default()
来创建一个DrawParameters
值,其所有字段都初始化为默认值,然后使用结构体的..
语法创建一个新的DrawParameters
值,只更改line_width
和point_size
字段,准备好将其传递给target.draw
。
如果一个类型T
实现了Default
,那么标准库会自动为Rc<T>
、Arc<T>
、Box<T>
、Cell<T>
、RefCell<T>
、Cow<T>
、Mutex<T>
和RwLock<T>
实现Default
。例如,Rc<T>
的默认值是一个指向T
类型默认值的Rc
。
如果一个元组类型的所有元素类型都实现了Default
,那么该元组类型也实现Default
,其默认值是一个包含每个元素默认值的元组。
Rust不会为结构体类型隐式实现Default
,但如果一个结构体的所有字段都实现了Default
,你可以使用#[derive(Default)]
自动为该结构体实现Default
。
# AsRef 和 AsMut
当一个类型实现了AsRef<T>
,这意味着你可以高效地从它那里借用一个&T
。AsMut
是对应可变引用的类似特性。它们的定义如下:
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
2
3
4
5
6
7
例如,Vec<T>
实现了AsRef<[T]>
,String
实现了AsRef<str>
。你还可以将String
的内容作为字节数组借用,所以String
也实现了AsRef<[u8]>
。
AsRef
通常用于使函数在接受参数类型时更加灵活。例如,std::fs::File::open
函数的声明如下:
fn open<P: AsRef<Path>>(path: P) -> Result<File>
open
真正需要的是一个&Path
,这是表示文件系统路径的类型。但有了这个签名,open
可以接受任何它能借用&Path
的类型,即任何实现了AsRef<Path>
的类型。这类类型包括String
、str
、操作系统接口字符串类型OsString
和OsStr
,当然还有PathBuf
和Path
;完整列表请查看库文档。这就是为什么你可以将字符串字面量传递给open
函数:
let dot_emacs = std::fs::File::open("/home/jimb/.emacs")?;
标准库中所有的文件系统访问函数都以这种方式接受路径参数。对于调用者来说,其效果类似于C++中的重载函数,尽管Rust在确定哪些参数类型可接受方面采用了不同的方法。
但事情并非这么简单。字符串字面量是&str
,而实现AsRef<Path>
的类型是str
,没有&
。正如我们在 “Deref和DerefMut” 中解释的,Rust不会尝试使用解引用强制转换来满足类型变量的约束,所以在这里它们也帮不上忙。
幸运的是,标准库包含了一个通用实现:
impl<'a, T, U> AsRef<U> for &'a T
where
T: AsRef<U>,
T: ?Sized,
U: ?Sized
{
fn as_ref(&self) -> &U {
(*self).as_ref()
}
}
2
3
4
5
6
7
8
9
10
换句话说,对于任何类型T
和U
,如果T: AsRef<U>
,那么&T: AsRef<U>
也成立:只需沿着引用继续操作即可。特别地,由于str: AsRef<Path>
,所以&str: AsRef<Path>
也成立。从某种意义上说,这是一种在检查AsRef
类型变量约束时获得有限形式解引用强制转换的方法。
你可能会认为,如果一个类型实现了AsRef<T>
,它也应该实现AsMut<T>
。然而,在某些情况下这并不合适。例如,我们提到过String
实现了AsRef<[u8]>
,这是有意义的,因为每个String
都有一个字节缓冲区,可以作为二进制数据来访问。但是,String
进一步保证了这些字节是格式良好的Unicode文本的UTF-8编码;如果String
实现了AsMut<[u8]>
,调用者就可以随意更改String
的字节,这样你就不能再保证String
是格式良好的UTF-8了。只有当修改给定的T
不会违反类型的不变性时,一个类型实现AsMut<T>
才有意义。
虽然AsRef
和AsMut
相当简单,但提供标准的、通用的引用转换特性可以避免更具体的转换特性的泛滥。如果可以实现AsRef<Foo>
,你就应该避免定义自己的AsFoo
特性。
# Borrow 和 BorrowMut
std::borrow::Borrow
特性与AsRef
类似:如果一个类型实现了Borrow<T>
,那么它的borrow
方法可以高效地从它那里借用一个&T
。但Borrow
施加了更多限制:一个类型只有在&T
的哈希值和比较方式与被借用的值相同时,才应该实现Borrow<T>
。(Rust不会强制这一点,这只是该特性文档中说明的意图。)这使得Borrow
在处理哈希表和树中的键,或者处理因其他原因需要进行哈希或比较的值时很有价值。
例如,从String
借用时,这种区别就很重要:String
实现了AsRef<str>
、AsRef<[u8]>
和AsRef<Path>
,但这三种目标类型通常具有不同的哈希值。只有&str
切片保证与等效的String
具有相同的哈希值,所以String
只实现了Borrow<str>
。
Borrow
的定义与AsRef
相同,只是名称不同:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
2
3
Borrow
是为解决泛型哈希表和其他关联集合类型的特定情况而设计的。例如,假设你有一个std::collections::HashMap<String, i32>
,用于将字符串映射到数字。这个表的键是String
类型,每个条目都拥有一个String
。在这个表中查找条目的方法签名应该是什么样的呢?下面是第一次尝试:
impl<K, V> HashMap<K, V>
where
K: Eq + Hash
{
fn get(&self, key: K) -> Option<&V> { ... }
}
2
3
4
5
6
这看起来很合理:要查找一个条目,你必须提供适合该表的键类型。但在这种情况下,K
是String
;这个签名会强制你每次调用get
时都按值传递一个String
,这显然很浪费。实际上,你只需要一个指向键的引用:
impl<K, V> HashMap<K, V>
where
K: Eq + Hash
{
fn get(&self, key: &K) -> Option<&V> { ... }
}
2
3
4
5
6
这稍微好一些,但现在你必须将键作为&String
传递,所以如果你想查找一个常量字符串,就必须这样写:
hashtable.get(&"twenty-two".to_string())
这很荒谬:它在堆上分配一个String
缓冲区,将文本复制到其中,只是为了将其作为&String
借用,传递给get
,然后再丢弃它。
传递任何可以与我们的键类型进行哈希和比较的东西就足够了,例如&str
就完全可以。所以最终的版本是这样的,这也是你在标准库中会看到的:
impl<K, V> HashMap<K, V>
where
K: Eq + Hash
{
fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Eq + Hash
{ ... }
}
2
3
4
5
6
7
8
9
10
换句话说,如果你可以将条目的键借用为&Q
,并且得到的引用的哈希值和比较方式与键本身相同,那么显然&Q
应该是可接受的键类型。由于String
实现了Borrow<str>
和Borrow<String>
,这个最终版本的get
方法允许你根据需要传递&String
或&str
作为键。
Vec<T>
和[T; N]
实现了Borrow<[T]>
。每个类似字符串的类型都允许借用其相应的切片类型:String
实现了Borrow<str>
,PathBuf
实现了Borrow<Path>
等等。标准库中所有的关联集合类型都使用Borrow
来决定哪些类型可以传递给它们的查找函数。
标准库包含一个通用实现,使得每个类型T
都可以从自身借用:T: Borrow<T>
。这确保了&K
始终是在HashMap<K, V>
中查找条目的可接受类型。
为了方便起见,每个&mut T
类型也实现了Borrow<T>
,像往常一样返回一个共享引用&T
。这允许你将可变引用传递给集合查找函数,而无需重新借用一个共享引用,模拟了Rust通常从可变引用到共享引用的隐式强制转换。
BorrowMut
特性是Borrow
对应可变引用的版本:
trait BorrowMut<Borrowed: ?Sized> : Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
2
3
对Borrow
的期望同样适用于BorrowMut
。
# From 和 Into
std::convert::From
和std::convert::Into
特性表示将一种类型的值消耗并返回另一种类型的值的转换。AsRef
和AsMut
特性是从一种类型借用另一种类型的引用,而From
和Into
则获取其参数的所有权,对其进行转换,然后将结果的所有权返回给调用者。
它们的定义非常对称:
trait Into<T> : Sized {
fn into(self) -> T;
}
trait From<T> : Sized {
fn from(other: T) -> Self;
}
2
3
4
5
6
7
标准库会自动为每种类型实现从自身到自身的平凡转换:每个类型T
都实现了From<T>
和Into<T>
。
虽然这两个特性只是提供了两种做同一件事的方式,但它们有不同的用途。
你通常使用Into
来使你的函数在接受参数时更加灵活。例如,如果你这样写:
use std::net::Ipv4Addr;
fn ping<A>(address: A) -> std::io::Result<bool>
where
A: Into<Ipv4Addr>
{
let ipv4_address = address.into();
...
}
2
3
4
5
6
7
8
9
那么ping
函数不仅可以接受Ipv4Addr
作为参数,还可以接受u32
或[u8; 4]
数组,因为这些类型恰好都实现了Into<Ipv4Addr>
。(有时将IPv4地址当作一个32位的值或一个4字节的数组很有用。)因为ping
函数只知道address
实现了Into<Ipv4Addr>
,所以在调用into
时,你不需要指定想要的具体类型;只有一种类型可能有效,所以类型推断会为你补充。
与上一节的AsRef
类似,其效果很像C++中的函数重载。有了前面定义的ping
函数,我们可以进行以下任何调用:
println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // 传递一个Ipv4Addr
println!("{:?}", ping([66, 146, 219, 98])); // 传递一个[u8; 4]
println!("{:?}", ping(0xd076eb94_u32)); // 传递一个u32
2
3
然而,From
特性起着不同的作用。from
方法作为一个通用构造函数,用于从某个其他单一值生成一个类型的实例。例如,Ipv4Addr
没有名为from_array
和from_u32
的两个方法,而是简单地实现了From<[u8;4]>
和From<u32>
,这样我们就可以这样写:
let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
let addr2 = Ipv4Addr::from(0xd076eb94_u32);
2
我们可以让类型推断来确定应用哪个实现。
有了合适的From
实现,标准库会自动实现相应的Into
特性。当你定义自己的类型时,如果它有单参数构造函数,你应该将它们写成针对适当类型的From<T>
实现;这样你会自动获得相应的Into
实现。
因为from
和into
转换方法获取其参数的所有权,所以转换可以重用原始值的资源来构造转换后的值。例如,假设你这样写:
let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
2
String
对Into<Vec<u8>>
的实现只是获取String
的堆缓冲区,并将其原封不动地重新用作返回向量的元素缓冲区。这种转换无需分配或复制文本。这是移动操作实现高效转换的另一个例子。
这些转换还提供了一种很好的方式,在不削弱受限类型保证的情况下,将受限类型的值转换为更灵活的类型。例如,String
保证其内容始终是有效的UTF-8;其可变方法受到严格限制,以确保你做的任何操作都不会引入错误的UTF-8。但在这个例子中,它有效地将String
“降级”为一块普通字节,你可以对其进行任何操作:也许你要对它进行压缩,或者将它与其他不是UTF-8的二进制数据组合。因为into
按值获取其参数,转换后text
不再初始化,这意味着我们可以自由访问之前String
的缓冲区,而不会破坏任何现有的String
。
然而,廉价转换并不是Into
和From
的约定内容。虽然AsRef
和AsMut
转换预期是廉价的,但From
和Into
转换可能会分配内存、复制数据或以其他方式处理值的内容。例如,String
实现了From<&str>
,它会将字符串切片复制到为String
新分配的堆缓冲区中。std::collections::BinaryHeap<T>
实现了From<Vec<T>>
,它会根据其算法要求对元素进行比较和重新排序。
?
运算符使用From
和Into
来帮助清理可能以多种方式失败的函数中的代码,在需要时自动将特定的错误类型转换为通用错误类型。
例如,假设有一个系统需要读取二进制数据,并将其中一部分以UTF-8文本形式表示的十进制数字进行转换。这意味着要使用std::str::from_utf8
和i32
的FromStr
实现,它们各自可能返回不同类型的错误。假设我们使用在第7章讨论错误处理时定义的GenericError
和GenericResult
类型,?
运算符会为我们进行转换:
type GenericError = Box<dyn std::error::Error + Send + Sync +'static>;
type GenericResult<T> = Result<T, GenericError>;
fn parse_i32_bytes(b: &[u8]) -> GenericResult<i32> {
Ok(std::str::from_utf8(b)?.parse::<i32>()?)
}
2
3
4
5
6
与大多数错误类型一样,Utf8Error
和ParseIntError
实现了Error
特性,标准库为我们提供了一个通用的From
实现,用于将任何实现了Error
的类型转换为Box<dyn Error>
,?
运算符会自动使用这个实现:
impl<'a, E: Error + Send + Sync + 'a> From<E>
for Box<dyn Error + Send + Sync + 'a> {
fn from(err: E) -> Box<dyn Error + Send + Sync + 'a> {
Box::new(err)
}
}
2
3
4
5
6
这将原本可能需要两个match
语句的相当长的函数变成了一行代码。
在From
和Into
被添加到标准库之前,Rust代码中充满了特定于单个类型的临时转换特性和构造方法。From
和Into
将这些约定规范化,你可以遵循这些约定,使你的类型更易于使用,因为你的用户已经熟悉它们了。其他库和语言本身也可以依赖这些特性,将其作为一种规范的、标准化的转换编码方式。
From
和Into
是不可失败的特性——它们的API要求转换不会失败。不幸的是,许多转换比这更复杂。例如,像i64
这样的大整数可以存储比i32
大得多的数字,如果没有一些额外信息,将像2_000_000_000_000i64
这样的数字转换为i32
就没有多大意义。进行简单的位运算转换,即丢弃前32位,通常不会得到我们期望的结果:
let huge = 2_000_000_000_000i64;
let smaller = huge as i32;
println!("{}", smaller); // -1454759936
2
3
处理这种情况有很多选择。根据具体情况,这样的“环绕”转换可能是合适的。另一方面,像数字信号处理和控制系统这样的应用,通常可以采用“饱和”转换,即大于最大可能值的数字被限制为该最大值。
# TryFrom 和 TryInto
由于不清楚这样的转换应该如何表现,Rust没有为i32
实现From<i64>
,也没有为任何其他可能会丢失信息的数值类型之间进行转换实现From
。相反,i32
实现了TryFrom<i64>
。
TryFrom
和TryInto
是From
和Into
的可失败版本,它们同样是相互对应的;实现了TryFrom
也就意味着实现了TryInto
。
它们的定义只比From
和Into
稍微复杂一点。
pub trait TryFrom<T> : Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
pub trait TryInto<T> : Sized {
type Error;
fn try_into(self) -> Result<T, Self::Error>;
}
2
3
4
5
6
7
8
9
try_into()
方法返回一个Result
,这样我们就可以在异常情况下(比如数字太大无法放入结果类型)选择如何处理:
use std::convert::TryInto;
// 溢出时饱和处理,而不是环绕
let smaller: i32 = huge.try_into().unwrap_or(i32::MAX);
2
3
如果我们还想处理负数情况,可以使用Result
的unwrap_or_else()
方法:
let smaller: i32 = huge.try_into().unwrap_or_else(|_| {
if huge >= 0 {
i32::MAX
} else {
i32::MIN
}
});
2
3
4
5
6
7
为你自己的类型实现可失败的转换也很容易。Error
类型可以根据具体应用的需求,简单或复杂。标准库使用一个空结构体,除了表明发生了错误之外不提供任何信息,因为唯一可能的错误就是溢出。另一方面,更复杂类型之间的转换可能想要返回更多信息:
impl TryInto<LinearShift> for Transform {
type Error = TransformError;
fn try_into(self) -> Result<LinearShift, Self::Error> {
if!self.normalized() {
return Err(TransformError::NotNormalized);
}
...
}
}
2
3
4
5
6
7
8
9
From
和Into
用于简单转换的类型关联,而TryFrom
和TryInto
在From
和Into
转换的简单性基础上,通过Result
提供了更具表达性的错误处理能力。这四个特性可以一起使用,在单个库中关联多种类型。
# ToOwned
给定一个引用,通常情况下,如果该类型实现了std::clone::Clone
,可以通过调用clone
来生成其引用目标的拥有所有权的副本。但是,如果你想克隆一个&str
或&[i32]
会怎样呢?你可能想要的是一个String
或Vec<i32>
,但Clone
的定义不允许这样做:根据定义,克隆一个&T
必须始终返回一个T
类型的值,而str
和[u8]
是未确定大小的类型,它们甚至不是函数可以返回的类型。
std::borrow::ToOwned
特性提供了一种更宽松的方式,将引用转换为拥有所有权的值:
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
2
3
4
与clone
必须精确返回Self
不同,to_owned
可以返回任何能借用&Self
的类型:Owned
类型必须实现Borrow<Self>
。你可以从Vec<T>
借用&[T]
,所以只要T
实现了Clone
(以便我们可以将切片的元素复制到向量中),[T]
就可以实现ToOwned<Owned=Vec<T>>
。同样,str
实现了ToOwned<Owned=String>
,Path
实现了ToOwned<Owned=PathBuf>
等等。
# Borrow 和 ToOwned 的应用:实用的 Cow
充分利用Rust需要仔细考虑所有权问题,比如函数应该通过引用还是值来接收参数。通常你可以选择其中一种方式,参数的类型也反映了你的决定。但在某些情况下,直到程序运行你才能决定是借用还是拥有所有权;std::borrow::Cow
类型(表示“写时克隆”)提供了一种解决方案。
它的定义如下:
enum Cow<'a, B: ?Sized>
where
B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
2
3
4
5
6
7
Cow<B>
要么借用一个指向B
的共享引用,要么拥有一个可以从中借用这种引用的值。
由于Cow
实现了Deref
,你可以像使用指向B
的共享引用一样对它调用方法:如果它是Owned
,它会借用对拥有的值的共享引用;如果它是Borrowed
,它直接返回所持有的引用。
你还可以通过调用to_mut
方法来获取对Cow
的值的可变引用,该方法返回一个&mut B
。如果Cow
恰好是Cow::Borrowed
,to_mut
会简单地调用引用的to_owned
方法来获取引用目标的副本,将Cow
转换为Cow::Owned
,并借用对新拥有的值的可变引用。这就是该类型名称所指的“写时克隆”行为。
同样,Cow
有一个into_owned
方法,如果有必要,它会将引用提升为拥有所有权的值,然后返回它,在这个过程中将所有权转移给调用者并消耗掉Cow
。
Cow
的一个常见用途是返回一个静态分配的字符串常量或一个计算得到的字符串。例如,假设你需要将一个错误枚举转换为一条消息。大多数变体可以用固定字符串处理,但其中一些变体有额外的数据应该包含在消息中。你可以返回一个Cow<'static, str>
:
use std::path::PathBuf;
use std::borrow::Cow;
fn describe(error: &Error) -> Cow<'static, str> {
match *error {
Error::OutOfMemory => "out of memory".into(),
Error::StackOverflow => "stack overflow".into(),
Error::MachineOnFire => "machine on fire".into(),
Error::Unfathomable => "machine bewildered".into(),
Error::FileNotFound(ref path) => {
format!("file not found: {}", path.display()).into()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这段代码使用Cow
的Into
实现来构造值。这个match
语句的大多数分支返回一个指向静态分配字符串的Cow::Borrowed
。但是当我们遇到FileNotFound
变体时,我们使用format!
来构造一个包含给定文件名的消息。这个match
分支产生一个Cow::Owned
值。
调用describe
且不需要更改值的调用者可以简单地将Cow
当作&str
来处理:
println!("Disaster has struck: {}", describe(&error));
需要拥有所有权的值的调用者可以轻松生成一个:
let mut log: Vec<String> = Vec::new();
...
log.push(describe(&error).into_owned());
2
3
使用Cow
有助于describe
函数及其调用者将分配操作推迟到必要时进行。