科学无非就是在自然界的多样性中寻求统一性(或者更确切地说,是在我们经验的多样性中寻求统一性)。用 Coleridge 的话说,诗歌、绘画、艺术,同样是在多样性中寻求统一性。 ——Jacob Bronowski
本章讲解了所谓的 Rust“实用工具”特型,这是标准库中各种特型的“百宝箱”,它们对 Rust 的编写方式有相当大的影响,所以,只有熟悉它们,你才能写出符合 Rust 语言惯例的代码并据此为你的 crate 设计公共接口,让用户认为这些接口是符合 Rust 风格的。 Rust 实用工具特型可分为三大类。 语言扩展特型 第 12 章中介绍的运算符重载特型能让你在自己的类型上使用 Rust 的表达式运算符,同样,还有其他几个标准库特型也是 Rust 的扩展点,允许你把自己的类型更紧密地集成进语言中。这类特型包括Drop、Deref 和 DerefMut,以及转换特型 From 和 Into。我们将在本章中讲解它们。 标记特型 这类特型多用作泛型类型变量的限界,以表达无法以其他方式捕获的约束条件。Sized 和 Copy 就属于这类特型。 公共词汇特型 这类特型不涉及任何编译器魔术,你完全可以在自己的代码中定义其等效特型。之所以定义它们,是为了给常见问题制定一些约定俗成的解决方案。这对 crate 和模块之间的公共接口来说特别有价值:通过减少不必要的变体,让接口更容易理解,也增加了把来自不同 crate 的特性轻易插接在一起的可能性,而且无须样板代码或自定义胶水代码。 这类特型包括 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。
13.1 Drop
当一个值的拥有者消失时,Rust 会丢弃(drop)该值。丢弃一个值就必须释放该值拥有的任何其他值、堆存储和系统资源。丢弃可能发生在多种情况下:当变量超出作用域时;在表达式语句的末尾;当截断一个向量时,会从其末尾移除元素;等等。 在大多数情况下,Rust 会自动处理丢弃值的工作。假设你定义了以下类型:
struct Appellation {
name: String,
nicknames: Vec<String>,
}
Appellation 拥有用作字符串内容和向量元素缓冲区的堆存储。每当 Appellation 被丢弃时,Rust 都会负责清理所有这些内容,无须你进行任何进一步的编码。但只要你想,也可以通过实现 std::ops::Drop 特型来自定义 Rust 该如何丢弃此类型的值:
trait Drop {
fn drop(&mut self);
}
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!("");
}
}
基于该实现,可以编写以下内容:
{
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");
}
当我们将第二个 Appellation 赋值给 a 时,就会丢弃第一个 Appellation,而当我们离开 a 的作用域时,就会丢弃第二个 Appellation。上述代码会打印出以下内容:
before assignment
Dropping Zeus (AKA cloud collector, king of the gods) at end of block Dropping Hera
Appellation 的 std::ops::Drop 实现只打印了一条消息,那么它的内存究竟是如何清理的呢?Vec 类型实现了 Drop,它会丢弃自己的每一个元素,然后释放它们占用的分配在堆上的缓冲区。
String 在内部使用 Vec
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?");
根据 complicated_condition 返回的是 true 还是 false,p 或 q 中的一个会最终拥有 Appellation,而另一个则会变成未初始化状态。这种差异决定了它是在 println! 之前还是之后丢弃 (因为 q 在 println! 之前就离开了作用域,而 p 则在 println! 之后离开的作用域)。虽然一个值可能会从一个地方移动到另一个地方,但 Rust 只会丢弃它一次。 除非正在定义某个拥有 Rust 不了解的资源类型,通常我们不需要自己实现 std::ops::Drop。例如,在 Unix 系统上,Rust 的标准库在内部使用了以下类型来表示操作系统的文件描述符:
struct FileDesc {
fd: c_int,
}
FileDesc 的 fd 字段是当程序完成时应该关闭的文件描述符的编 号,c_int 是 i32 的别名。标准库为 FileDesc 实现的 Drop 如下所示:
impl Drop for FileDesc {
fn drop(&mut self) {
let _ = unsafe { libc::close(self.fd) };
}
}
这里,libc::close 是 C 库中 close 函数的 Rust 名称。Rust 代码只能在 unsafe 块中调用 C 函数,因此在这里使用了一个 unsafe 块。 如果一个类型实现了 Drop,就不能再实现 Copy 特型了。如果类型是 Copy 类型,就表示简单的逐字节复制足以生成该值的独立副本。但是,对同一份数据多次调用同一个 drop 方法显然是错误的。 标准库预导入中包含一个丢弃值的函数 drop,但它的定义一点儿也不神奇:
fn drop<T>(_x: T) {}
换句话说,它会按值接受参数,从调用者那里获得所有权,然后什么也不做。当 _x 超出作用域时,Rust 自然会丢弃它的值,这跟对任何其他变量的操作一样。
13.2 Sized
固定大小类型是指其每个值在内存中都有相同大小的类型。Rust 中的几乎所有类型都是固定大小的,比如每个 u64 占用 8 字节,每个
(f32, f32, f32) 元组占用 12 字节。甚至枚举也是有大小的,也就是说,无论实际存在的是哪个变体,枚举总会占据足够的空间来容纳其最大的变体。尽管 Vec

图 13-1:对无固定大小的值的引用
Rust 中另一种常见的无固定大小类型是 dyn 类型,它是特型对象的引用目标。正如我们在 11.1.1 节中所解释的那样,特型对象是指向实现了给定特型的某个值的指针。例如,类型 &dyn
std::io::Write 和 Box
struct RcBox<T: ?Sized> {
ref_count: usize,
value: T,
}
Rc
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;
在将值传给函数时会发生隐式转换,这样你就可以将
&RcBox
fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
这将生成以下输出。
For your enjoyment: lunch
13.3 Clone
std::clone::Clone 特型适用于可复制自身的类型。Clone 定义如下:
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
clone 方法应该为 self 构造一个独立的副本并返回它。由于此方法的返回类型是 Self,并且函数本来也不可能返回无固定大小的值,因此 Clone 特型也是扩展自 Sized 特型的,进而导致其实现代码中的 Self 类型被限界成了 Sized。
克隆一个值通常还需要为它拥有的任何值分配副本,因此 clone 无
论在时间消耗还是内存占用方面都是相当昂贵的。例如,克隆
Vec
13.4 Copy
在第 4 章中,我们曾解释说,对于大多数类型,赋值时会移动值,而不是复制它们。移动值可以更简单地跟踪它们所拥有的资源。但在 4.3 节中,我们指出了例外情况:不拥有任何资源的简单类型可以是 Copy 类型,对这些简单类型赋值会创建源的副本,而不会移动值并使源回到未初始化状态。 当时,我们没有充分解释 Copy 类型到底是什么,现在可以告诉你了:如果一个类型实现了 std::marker::Copy 标记特型,那么它就是 Copy 类型,其定义如下所示:
trait Copy: Clone {}
对于你自己的类型,这当然很容易实现:
impl Copy for MyType {}
但由于 Copy 是一种对语言有着特殊意义的标记特型,因此只有当类型需要一个浅层的逐字节复制时,Rust 才允许它实现 Copy。拥有任 何其他资源(比如堆缓冲区或操作系统句柄)的类型都无法实现 Copy。 任何实现了 Drop 特型的类型都不能是 Copy 类型。Rust 认为如果一个类型需要特殊的清理代码,那么就必然需要特殊的复制代码,因此不能是 Copy 类型。 与 Clone 一样,可以使用 #[derive(Copy)] 让 Rust 为你派生出 Copy 实现。你会经常看到同时使用 #[derive(Copy, Clone)] 进行派生的代码。 在允许一个类型成为 Copy 类型之前务必慎重考虑。尽管这样做能让该类型更易于使用,但也对其实现施加了严格的限制。如果复制的开销很高,那么就不适合进行隐式复制。4.3 节曾详细解释过这些因素。
13.5 Deref 与 DerefMut
通过实现 std::ops::Deref 特型和 std::ops::DerefMut 特型,可以指定像 * 和 . 这样的解引用运算符在你的类型上的行为。
像 Box
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
deref 方法会接受 &Self 引用并返回 &Self::Target 引用,而 deref_mut 方法会接受 &mut Self 引用并返回 &mut
Self::Target 引用。Target 应该是 Self 包含、拥有或引用的资源:对于 Box
struct Selector<T> {
/// 在这个`Selector`中可用的元素 elements: Vec<T>,
/// `elements`中“当前”(current)元素的索引
/// `Selector`的行为类似于指向当前元素的指针
current: usize,
}
要让 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]
}
}
给定上述实现,可以像下面这样使用 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']);
Deref 特型和 DerefMut 特型旨在实现诸如 Box、Rc 和 Arc 之类的智能指针类型,以及其拥有型版本会频繁通过引用来使用的类型
(比如 Vec
let s = Selector {
elements: vec!["good", "bad", "ugly"],
current: 2,
};
fn show_it(thing: &str) {
println!("{}", thing);
}
show_it(&s);
在调用 show_it(&s) 时,Rust 发现了一个类型为 &Selector<&str> 的实参(argument)和一个类型为 &str 的形参(parameter),据此找到了这个 Deref
在需要做出区分的上下文中,本书选择将 argument 和 parameter 分别译为“实参”和 “形参”。除此之外,在表示一般性概念或不会导致混淆的情况下,二者均译为“参数”。 ——编者注但是,如果将 show_it 改成泛型函数,Rust 突然就报错了:
use std::fmt::Display;
fn show_it_generic<T: Display>(thing: T) {
println!("{}", thing);
}
show_it_generic(&s);
Rust 报错说:
error: `Selector<&str>` doesn't implement `std::fmt::Display` |
31 | show_it_generic(&s);
| ^^
| |
| `Selector<&str>` cannot be formatted with
| the default formatter
| help: consider adding dereference here:
`&*s` |
note: required by a bound in `show_it_generic` |
30 | fn show_it_generic<T: Display>(thing: T) { println!("{}",
thing); }
| ^^^^^^^ required by this bound | in `show_it_generic`
这可能会令人困惑:为什么仅仅把函数改成泛型形式就会引入错误 呢?Selector<&str> 本身确实没有实现 Display,但它解引用成了 &str,而 &str 实现了 Display。 由于你要传入一个类型为 &Selector<&str> 的实参并且函数的形参类型为 &T,因此类型变量 T 必然是 Selector<&str>。然后, Rust 会检查这是否满足 T: Display 限界,但因为它不会通过隐式解引用来满足类型变量的限界,所以这个检查失败了。要解决此问题,可以使用 as 运算符进行显式转换:
show_it_generic(&s as &str);
或者,正如编译器建议的那样,可以使用 &* 进行强制转换。
show_it_generic(&*s);
13.6 Default
显然,某些类型具有合理的默认值:向量或字符串默认为空、数值默认为 0、Option 默认为 None,等等。这样的类型都可以实现 std::default::Default 特型:
trait Default {
fn default() -> Self;
}
default 方法只会返回一个 Self 类型的新值。为 String 实现 Default 的代码一目了然:
impl Default for String {
fn default() -> String {
String::new()
}
}
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);
闭包 |&n| n & (n-1) == 0 会使用一些位操作来识别哪些数值是 2 的幂,并且 partition 会使用它来生成两个 HashSet。不过, partition 显然不是专属于 HashSet 的,你可以用它来生成想要的任何种类的集合,只要该集合类型能够实现 Default 以生成一个初始的空集合,并且实现 Extend
let (upper, lower): (String, String) = "Great Teacher Onizuka"
.chars()
.partition(|&c| c.is_uppercase());
assert_eq!(upper, "GTO");
assert_eq!(lower, "reat eacher nizuka");
Default 的另一个常见用途是为表示大量参数集合的结构体生成默认值,其中大部分参数通常不用更改。例如,glium crate 为强大而复杂的 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();
这会调用 Default::default() 来创建一个 DrawParameters 值,该值会使用其所有字段的默认值进行初始化,然后使用结构体的
.. 语法创建出一个更改了 line_width 字段和 point_size 字段的新值,最后就可以把它传给 target.draw 了。
如果类型 T 实现了 Default,那么标准库就会自动为 Rc
13.7 AsRef 与 AsMut
如果一个类型实现了 AsRef
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
例如,Vec
fn open<P: AsRef<Path>>(path: P) -> Result<File>
open 真正想要的是 &Path,即代表文件系统路径的类型。有了这个函数签名,open 就能接受可以从中借入 &Path 的一切,也就是实现了 AsRef
let dot_emacs = std::fs::File::open("/home/jimb/.emacs")?;
标准库的所有文件系统访问函数都会以这种方式接受路径参数。对调用者来说,其效果类似于 C++ 中的重载函数,只不过 Rust 采用的是另一种方式来确定可接受的参数类型。但这还不是全部。字符串字面量是 &str,实现了 AsRef
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()
}
}
换句话说,对于任意类型 T 和 U,只要满足 T: AsRef,就必然满足 &T: AsRef:只需追踪引用并像以前那样继续处理即可。特别是,如果满足 str: AsRef
13.8 Borrow 与 BorrowMut
std::borrow::Borrow 特型类似于 AsRef:如果一个类型实现了 Borrow
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
Borrow 旨在解决具有泛型哈希表和其他关联集合类型的特定情况。假设你有一个 std::collections::HashMap
impl<K, V> HashMap<K, V>
where
K: Eq + Hash,
{
fn get(&self, key: K) -> Option<&V> {}
}
这很合理:要查找条目,就必须为表提供适当类型的键。但在这里, K 是 String,这种签名会强制你将 String 按值传给对 get 的每次调用,这显然是一种浪费。你真正需要的只是此键的引用:
impl<K, V> HashMap<K, V>
where
K: Eq + Hash,
{
fn get(&self, key: &K) -> Option<&V> {}
}
这稍微好一点儿了,但现在你必须将键作为 &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,
{
}
}
换句话说,只要可以借入一个条目的键充当 &Q,并且对生成的引用进行哈希和比较的方式与键本身一致,&Q 显然就是可接受的键类
型。由于 String 实现了 Borrow
trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
刚才讲过的对 Borrow 的要求同样适用于 BorrowMut。
13.9 From 与 Into
std::convert::From 特型和 std::convert::Into 特型表示类型转换,这种转换会接受一种类型的值并返回另一种类型的值。 AsRef 特型和 AsMut 特型用于从一种类型借入另一种类型的引用, 而 From 和 Into 会获取其参数的所有权,对其进行转换,然后将转换结果的所有权返回给调用者。 From 和 Into 的定义是对称的:
trait Into<T>: Sized {
fn into(self) -> T;
}
trait From<T>: Sized {
fn from(other: T) -> Self;
}
标准库自动实现了从每种类型到自身的简单转换:每种类型 T 都实现了 From
use std::net::Ipv4Addr;
fn ping< A>(address: A) -> std::io::Result<bool>
where
A: Into<Ipv4Addr>,
{
let ipv4_address = address.into();
}
那么 ping 不仅可以接受 Ipv4Addr 作为参数,还可以接受 u32 或 [u8; 4] 数组,因为这些类型都恰好实现了
Into
println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // 传入一个 Ipv4Addr println!("{:?}", ping([66, 146, 219, 98])); // 传入一个
[u8; 4]
println!("{:?}", ping(0xd076eb94_u32)); // 传入一个 u32
而 From 特型扮演着另一种角色。from 方法会充当泛型构造函数,用于从另一个值生成本类型的实例。例如,虽然 Ipv4Addr 有两个
名为 from_array 和 from_u32 的方法,但 From 只是简单地实现了 From<[u8;4]> 和 From
let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
let addr2 = Ipv4Addr::from(0xd076eb94_u32);
可以让类型推断找出适用于此的实现。
给定适当的 From 实现,标准库会自动实现相应的 Into 特型。当你定义自己的类型时,如果它具有某些单参数构造函数,那么就应该将它们写成适当类型的 From
let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
String 的 Into
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>()?)
}
与大多数错误类型一样,Utf8Error 和 ParseIntError 也实现
了 Error 特型,标准库为我们提供了 From 的通用实现,用于将任何实现了 Error 的类型转换为 Box
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)
}
}
这能把具有两个 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
有很多选项可以处理这种情况。根据上下文的不同,“回绕型”转换可能比较合适。另外,像数字信号处理和控制系统这样的应用程序通常会使用“饱和型”转换,它会把比可能的 大值还要大的数值限制为 大值。
13.10 TryFrom 与 TryInto
由于转换的行为方式不够清晰,因此 Rust 没有为 i32 实现 From
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>;
}
try_into() 方法给了我们一个 Result,因此我们可以选择在异常情况下该怎么做,比如处理一个因为太大而无法放入结果类型的数值:
// 溢出时饱和,而非回绕
let smaller: i32 = huge.try_into().unwrap_or(i32::MAX);
如果还想处理负数的情况,那么可以使用 Result 的 unwrap_or_else() 方法:
let smaller: i32 =
huge.try_into()
.unwrap_or_else(|_| if huge >= 0 { i32::MAX } else { i32::MIN });
为你自己的类型实现容错的转换也很容易。Error 类型既可以很简单,也可以很复杂,具体取决于特定应用程序的要求。标准库使用的是一个空结构体,除了发生过错误这一事实之外没有提供任何信息,因为唯一可能的错误就是溢出。另外,更复杂类型之间的转换可能需要返回更多信息:
impl TryInto<LinearShift> for Transform {
type Error = TransformError;
fn try_into(self) -> Result<LinearShift, Self::Error> {
if !self.normalized() {
return Err(TransformError::NotNormalized);
}
}
}
From 和 Into 可以将类型与简单转换关联起来,而 TryFrom 和 TryInto 通过 Result 提供的富有表现力的错误处理扩展了 From 和 Into 的简单转换。这 4 个特型可以一起使用,在同一个 crate 中关联多个类型。
13.11 ToOwned
给定一个引用,如果此类型实现了 std::clone::Clone,则生成其引用目标的拥有型副本的常用方法是调用 clone。但是当你想克隆一个 &str 或 &[i32] 时该怎么办呢?你想要的可能是 String 或 Vec
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
与必须精确返回 Self 类型的 clone 不同,to_owned 可以返回任何能让你从中借入 &Self 的类型:Owned 类型必须实现 Borrow
13.12 Borrow 与 ToOwned 的实际运用:谦卑的 Cow
2指不会主动占有资源,直到确有必要。——译者注 要想用好 Rust,就必然涉及对所有权问题的透彻思考,比如函数应该通过引用还是值接受参数。通常你可以任选一种方式,让参数的类型反映你的决定。但在某些情况下,在程序开始运行之前你无法决定是该借用还是该拥有,std::borrow::Cow 类型(用于“写入时克隆”,clone on write 的缩写)提供了一种兼顾两者的方式。 std::borrow::Cow 的定义如下所示:
enum Cow<'a, B: ?Sized>
where
B: ToOwned,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Cow 要么借入对 B 的共享引用,要么拥有可供借入此类引用的值。由于 Cow 实现了 Deref,因此你可以像对 B 的共享引用一样调用它的方法:如果它是 Owned,就会借入对拥有值的共享引用;如果它是 Borrowed,就会转让自己持有的引用。 还可以通过调用 Cow 的 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::borrow::Cow;
use std::path::PathBuf;
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(),
}
}
上述代码使用了 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());
使用 Cow,describe 及其调用者可以把分配的时机推迟到确有必要的时候。