Rust基础拾遗--所有权与移动、引用、特性与泛型、实用工具特型、闭包


前言

   通过Rust程序设计-第二版笔记的形式对Rust相关重点知识进行汇总,读者通读此系列文章就可以轻松的把该语言基础捡起来。


1.所有权与移动

谈及内存管理,希望编程语言具备两个特点:

  • 希望内存能在我们选定的时机及时释放,这使我们能控制程序的内存消耗;
  • 在对象被释放后,不希望继续使用指向它的指针,这是未定义行为,会导致崩溃和安全漏洞。

Rust 通过限制程序使用指针的方式出人意料地打破了这种困局。

Rust 的编译期检查器有能力验证程序中是否存在内存安全错误:悬空指针、重复释放、使用未初始化的内存等。

Rust 程序中的缺陷不会导致一个线程破坏另一个线程的数据,进而在系统中的无关部分引入难以重现的故障。

相关问题:

  1. 看看所有权在概念层和实现层分别意味着什么?
  2. 如何在各种场景中跟踪所有权的变化?
  3. 哪些情况下要改变或打破其中的一些规则,以提供更大的灵活性?

1.1 所有权

拥有对象意味着可以决定何时释放此对象:当销毁拥有者时,它拥有的对象也会随之销毁。

变量拥有自己的值,当控制流离开声明变量的块时,变量就会被丢弃,因此它的值也会一起被丢弃。例如:

fn print_padovan() {
    let mut padovan = vec![1,1,1]; // 在此分配
    for i in 3..10 {
        let next = padovan[i-3] + padovan[i-2];
        padovan.push(next);
    }
    println!("P(1..10) = {:?}", padovan);
} // 在此丢弃

变量 padovan 的类型为 Vec<i32>。在内存中,padovan 的最终值如图所示。
在这里插入图片描述 跟C++ std::string 非常相似,不过缓冲区中的元素都是 32 位整数,而不是字符。

   Rust 的 Box 类型是所有权的另一个例子。Box 是指向存储在堆上的 T 类型值的指针。可以调用 Box::new(v) 分配一些堆空间,将值 v 移入其中,并返回一个指向该堆空间的 Box。因为 Box 拥有它所指向的空间,所以当丢弃 Box 时,也会释放此空间。

在堆中分配一个元组:

{
    let point = Box::new((0.625, 0.5)); // 在此分配了point
    let label = format!("{:?}", point); // 在此分配了label
    assert_eq!(label, "(0.625, 0.5)");
} // 在此全都丢弃了

   当程序调用 Box::new 时,它会在堆上为由两个 f64 值构成的元组分配空间,然后将其参数 (0.625, 0.5) 移进去,并返回指向该空间的指针。当控制流抵达对 assert_eq! 的调用时,栈帧如图 4-3 所示。

在这里插入图片描述
   栈帧本身包含变量 pointlabel,其中每个变量都指向其拥有的堆中内存。当丢弃它们时,它们拥有的堆中内存也会一起被释放。

就像变量拥有自己的值一样,结构体拥有自己的字段,元组、数组和向量则拥有自己的元素。

struct Person { name: String, birth: i32 }

let mut composers = Vec::new();
composers.push(Person { name: "Palestrina".to_string(),
birth: 1525 });
composers.push(Person { name: "Dowland".to_string(),
birth: 1563 });
composers.push(Person { name: "Lully".to_string(),
birth: 1632 });
for composer in &composers {
    println!("{}, born {}", composer.name, composer.birth);
}

   在这里,composers 是一个 Vec<Person>,即由结构体组成的向量,每个结构体都包含一个字符串和数值。在内存中,composers 的最终值如图所示。

在这里插入图片描述
   这里有很多所有权关系,但每个都一目了然:composers 拥有一个向量,向量拥有自己的元素,每个元素都是一个 Person 结构体,每个结构体都拥有自己的字段,并且字符串字段拥有自己的文本。当控制流离开声明 composers 的作用域时,程序会丢弃自己的值并将整棵所有权树一起丢弃。如果还存在其他类型的集合(可能是 HashMap 或 BTreeSet),那么处理的方式也是一样的。

   现在,回过头来思考一下刚刚介绍的这些所有权关系的重要性。每个值都有一个唯一的拥有者,因此很容易决定何时丢弃它。但是每个值可能会拥有许多其他值,比如向量 composers 会拥有自己的所有元素。这些值还可能拥有其他值:composers 的每个元素都各自拥有一个字符串,该字符串又拥有自己的文本。

   由此可见,拥有者及其拥有的那些值形成了一棵树:值的拥有者是值的父节点,值拥有的值是值的子节点。每棵树的总根都是一个变量,当该变量超出作用域时,整棵树都将随之消失。可以在 composers 的图中看到这样的所有权树:它既不是“搜索树”那种数据结构意义上的“树”,也不是由 DOM 元素构成的 HTML 文档。相反,我们有一棵由混合类型构建的树,Rust 的单一拥有者规则将禁止任何可能让它们排列得比树结构更复杂的可能性。Rust 程序中的每一个值都是某棵树的成员,树根是某个变量。

   Rust 程序通常不需要像 C 程序和 C++ 程序那样显式地使用 free 和 delete 来丢弃值。在 Rust 中丢弃一个值的方式就是从所有权树中移除它:或者离开变量的作用域,或者从向量中删除一个元素,或者执行其他类似的操作。这样一来,Rust 就会确保正确地丢弃该值及其拥有的一切。

   从某种意义上说,Rust 确实不如其他语言强大:其他所有实用的编程语言都允许你构建出任意复杂的对象图,这些对象可以用你认为合适的方式相互引用。但正是因为 Rust 不那么强大,所以编辑器对你的程序所进行的分析才能更强大。Rust 的安全保证之所以可行,是因为在你的代码中可能出现的那些关系都更可控。这是之前提过的 Rust 的“激进赌注”的一部分:实际上,Rust 声称,解决问题的方式通常是灵活多样的,因此总是会有一些完美的解决方案能同时满足它所施加的限制。

迄今为止,我们已经解释过的这些所有权概念仍然过于严格,还处理不了某些场景。Rust 从

所有权概念仍然过于严格,还处理不了某些场景,但是,Rust 从几个方面扩展了这种简单的思想。

  • 可以将值从一个拥有者转移给另一个拥有者。这允许你构建、重新排列和拆除树形结构。
  • 像整数、浮点数和字符这样的非常简单的类型,不受所有权规则的约束。这些称为 Copy 类型。
  • 标准库提供了引用计数指针类型 Rc 和 Arc,它们允许值在某些限制下有多个拥有者。
  • 可以对值进行“借用”(borrow),以获得值的引用。这种引用是非拥有型指针,有着受限的生命周期。

1.2 移动

   在 Rust 中,对大多数类型来说,像为变量赋值、将其传给函数或从函数返回这样的操作都不会复制值,而是会移动值。源会把值的所有权转移给目标并变回未初始化状态,改由目标变量来控制值的生命周期。Rust 程序会以每次只移动一个值的方式建立和拆除复杂的结构。

类比C++ 代码:

using namespace std;
vector<string> s = { "udon", "ramen", "soba" };
vector<string> t = s;
vector<string> u = s;

s 的原始值在内存中如图所示。

在这里插入图片描述
当程序将 s 赋值给 t 和 u 时会发生什么?

   在 C++ 中,把 std::vector 赋值给其他元素会生成一个向量的副本,std::string 的行为也类似。所以当程序执行到这段代码的末尾时,它实际上已经分配了 3 个向量和 9 个字符串,如图 所示(在c++中将s赋值给t和u的结果)。

在这里插入图片描述
   理论上,如果涉及某些特定的值,那么 C++ 中的赋值可能会消耗超乎想象的内存和处理器时间。然而,其优点是程序很容易决定何时释放这些内存:当变量超出作用域时,此处分配的所有内容都会自动清除。

   从某种意义上说,C++ 和 Python 选择了相反的权衡:Python 以需要引用计数(以及更广泛意义上的垃圾回收)为代价,让赋值的开销变得非常低。C++ 则选择让全部内存的所有权保持清晰,而代价是在赋值时要执行对象的深拷贝。一般来说,C++ 程序员不太热衷这种选择:深拷贝的开销可能很昂贵,而且通常有更实用的替代方案。

那么类似的程序在 Rust 中会怎么做呢?

请看如下代码:

let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s;
let u = s;

   与 C 和 C++ 一样,Rust 会将纯字符串字面量(如 “udon”)放在只读内存中,因此为了与 C++ 示例和 Python 示例进行更清晰的比较,此处调用了 to_string 以获取堆上分配的 String 值。

   在执行了 s 的初始化之后,由于 Rust 和 C++ 对向量和字符串使用了类似的表示形式,因此情况看起来就和 C++ 中一样,如图 所示(Rust如何表示内存中的字符串向量)。

在这里插入图片描述
   但要记住,在 Rust 中,大多数类型的赋值会将值从源转移给目标,而源会回到未初始化状态。因此在初始化 t 之后,程序的内存如图 所示(Rust中将s赋值给t的结果)。
在这里插入图片描述这里发生了什么?
   初始化语句 let t = s; 将向量的 3 个标头字段从 s 转移给了 t,现在 t 拥有此向量。向量的元素保持原样,字符串也没有任何变化。每个值依然只有一个拥有者,尽管其中一个已然易手。整个过程中没有需要调整的引用计数,不过编译器现在会认为 s 是未初始化状态。

   那么当我们执行初始化语句 let u = s; 时会发生什么呢?这会将尚未初始化的值 s 赋给 u。Rust 明智地禁止使用未初始化的值,因此编译器会拒绝此代码并报告如下错误:

error: use of moved value: `s`

   思考一下 Rust 在这里使用移动语义的影响。与 Python 一样,赋值操作开销极低:程序只需将向量的三字标头从一个位置移到另一个位置即可。但与 C++ 一样,所有权始终是明确的:程序不需要引用计数或垃圾回收就能知道何时释放向量元素和字符串内容。

   代价是如果需要同时访问它们,就必须显式地要求复制。如果想达到与 C++ 程序相同的状态(每个变量都保存一个独立的结构副本),就必须调用向量的 clone 方法,该方法会执行向量及其元素的深拷贝:

let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s.clone();
let u = s.clone();

1.2.1 更多移动类操作

   在先前的例子中,我们已经展示了如何初始化工作——在变量进入 let 语句的作用域时为它们提供值。给变量赋值则与此略有不同,如果你将一个值转移给已初始化的变量,那么 Rust 就会丢弃该变量的先前值。例如:

let mut s = "Govinda".to_string();
s = "Siddhartha".to_string(); // 在这里丢弃了值"Govinda"

在上述代码中,当程序将字符串 “Siddhartha” 赋值给 s 时,它的先前值 “Govinda” 会首先被丢弃。但请考虑以下代码:

let mut s = "Govinda".to_string();
let t = s;
s = "Siddhartha".to_string(); // 这里什么也没有丢弃

这一次,t 从 s 接手了原始字符串的所有权,所以当给 s 赋值时,它是未初始化状态。这种情况下不会丢弃任何字符串。

   这个例子中使用了初始化和赋值,因为它们很简单,但 Rust 还将“移动”的语义应用到了几乎所有对值的使用上。例如,将参数传给函数会将所有权转移给函数的参数、从函数返回一个值会将所有权转移给调用者、构建元组会将值转移给元组。

例如,我们在构建 composers 向量时,是这样写的:

struct Person { name: String, birth: i32 }

let mut composers = Vec::new();
composers.push(Person { name: "Palestrina".to_string(),
 birth: 1525 });

这段代码展示了除初始化和赋值之外发生移动的几个地方。

从函数返回值

   调用 Vec::new() 构造一个新向量并返回,返回的不是指向此向量的指针,而是向量本身:它的所有权从 Vec::new 转移给了变量 composers。同样,to_string 调用返回的是一个新的 String 实例。

构造出新值

新 Person 结构体的 name 字段是用 to_string 的返回值初始化的。该结构体拥有这个字符串的所有权。

将值传给函数

   整个 Person 结构体(不是指向它的指针)被传给了向量的 push 方法,此方法会将该结构体移动到向量的末尾。向量接管了 Person 的所有权,因此也间接接手了 name 这个 String 的所有权。

   像这样移动值乍一看可能效率低下,但有两点需要牢记。首先,移动的永远是值本身,而不是这些值拥有的堆存储。对于向量和字符串,值本身就是指单独的“三字标头”,幕后的大型元素数组和文本缓冲区仍然位于它们在堆中的位置。其次,Rust 编译器在生成代码时擅长“看穿”这一切动作。在实践中,机器码通常会将值直接存储在它应该在的位置。

1.2.2 移动与控制流

   前面的例子中都有非常简单的控制流,**那么该如何在更复杂的代码中移动呢?**一般性原则是,如果一个变量的值有可能已经移走,并且从那以后尚未明确赋予其新值,那么它就可以被看作是未初始化状态。如果一个变量在执行了 if 表达式中的条件后仍然有值,那么就可以在这两个分支中使用它:

let x = vec![10, 20, 30];
if c {
    f(x); // ……可以在这里移动x
} else {
    g(x); // ……也可以在这里移动x
}
h(x); // 错误:只要任何一条路径用过它,x在这里就是未初始化状态

出于类似的原因,禁止在循环中进行变量移动:

let x = vec![10, 20, 30];
while f() {
    g(x); // 错误:x已经在第一次迭代中移动出去了,在第二次迭代中,它成了未初始化状态
}

也就是说,除非在下一次迭代中明确赋予 x 一个新值,否则就会出错。

let mut x = vec![10, 20, 30];
while f() {
    g(x); // 从x移动出去了
    x = h(); // 赋予x一个新值
}
e(x);

1.2.3 移动与索引内容

   移动会令其来源变成未初始化状态,因为目标将获得该值的所有权。但并非值的每种拥有者都能变成未初始化状态。例如,考虑以下代码:

//  构建一个由字符串"101"、"102"……"105"组成的向量
let mut v = Vec::new();
for i in 101 .. 106 {
 v.push(i.to_string());
}

//  从向量中随机抽取元素
let third = v[2]; //  错误:不能移动到 Vec 索引结构之外<sup><b>3</b></sup>
let fifth = v[4]; //  这里也一样

// 注:v[2] 而非 &v[2]

   为了解决这个问题,Rust 需要以某种方式记住向量的第三个元素和第五个元素是未初始化状态,并要跟踪该信息直到向量被丢弃。通常的解决方案是,让每个向量都携带额外的信息来指示哪些元素是活动的,哪些元素是未初始化的。这显然不是系统编程语言应该做的。向量应该只是向量,不应该携带额外的信息或状态。

事实上,Rust 会拒绝前面的代码并报告如下错误:

error: cannot move out of index of `Vec`

   移动第五个元素时 Rust 也会报告类似的错误。在这条错误消息中,Rust 还建议使用引用,因为你可能只是想访问该元素而不是移动它,这通常确实是你想要做的。但是,**如果真想将一个元素移出向量该怎么办呢?**需要找到一种在遵循类型限制的情况下执行此操作的方法。以下是 3 种可能的方法:

// 构建一个由字符串"101"、"102"……"105"组成的向量
let mut v = Vec::new();
for i in 101 .. 106 {
 v.push(i.to_string());
}

// 方法一:从向量的末尾弹出一个值:
let fifth = v.pop().expect("vector empty!");
assert_eq!(fifth, "105");

// 方法二:将向量中指定索引处的值与最后一个值互换,并把前者移动出来:
let second = v.swap_remove(1);
assert_eq!(second, "102");

// 方法三:把要取出的值和另一个值互换:
let third = std::mem::replace(&mut v[2], "substitute".to_string());
assert_eq!(third, "103");

// 看看向量中还剩下什么
assert_eq!(v, vec!["101", "104", "substitute"]);

每种方法都能将一个元素移出向量,但仍会让向量处于完全填充状态,只是向量可能会变小。

像 Vec 这样的集合类型通常也会提供在循环中消耗所有元素的方法:

let v = vec!["liberté".to_string(),
 "égalité".to_string(),
 "fraternité".to_string()];

for mut s in v {
    s.push('!');
    println!("{}", s);
}

   当我们将向量直接传给循环(如 for ... in v)时,会将向量从 v 中移动出去,让 v 变成未初始化状态。for 循环的内部机制会获取向量的所有权并将其分解为元素。在每次迭代中,循环都会将另一个元素转移给变量 s。由于 s 现在拥有字符串,因此可以在打印之前在循环体中修改它。在循环的过程中,向量本身对代码不再可见,因此也就无法观察到它正处在某种部分清空的状态。

   如果需要从拥有者中移出一个编译器无法跟踪的值,那么可以考虑将拥有者的类型更改为能动态跟踪自己是否有值的类型。例如,下面是前面例子的一个变体:

struct Person { name: Option<String>, birth: i32 }

let mut composers = Vec::new();
composers.push(Person { name: Some("Palestrina".to_string()),
 birth: 1525 });

但不能像下面这样做:

let first_name = composers[0].name;

   这只会引发与前面一样的“无法移动到索引结构之外”错误。但是因为已将 name 字段的类型从 String 改成了 Option,所以这意味着 None 也是该字段要保存的合法值。因此,可以像下面这样做:

let first_name = std::mem::replace(&mut composers[0].name, None);
assert_eq!(first_name, Some("Palestrina".to_string()));
assert_eq!(composers[0].name, None);

   replace 调用会移出 composers[0].name 的值,将 None 留在原处,并将原始值的所有权转移给其调用者。事实上,这种使用 Option 的方式非常普遍,所以该类型专门为此提供了一个 take 方法,以便更清晰地写出上述操作,如下所示:

let first_name = composers[0].name.take();

这个 take 调用与之前的 replace 调用具有相同的效果。

1.3 Copy 类型:关于移动的例外情况

本章所展示的值移动示例都涉及向量、字符串和其他可能占用大量内存且复制成本高昂的类型。

   移动能让这些类型的所有权清晰且赋值开销极低。但对于像整数或字符这样的简单类型,如此谨小慎微的处理方式确实没什么必要。下面来比较一下用 String 进行赋值和用 i32 进行赋值时内存中有什么不同:

let string1 = "somnambulance".to_string();
let string2 = string1;

let num1: i32 = 36;
let num2 = num1;

运行这段代码后,内存如图 所示(用String赋值会移动值,而用i32赋值会复制值)。

在这里插入图片描述
   与前面的向量一样,赋值会将 string1转移给string2,这样就不会出现两个字符串负责释放同一个缓冲区的情况。但是,num1 和 num2 的情况有所不同。i32 只是内存中的几字节,它不拥有任何堆资源,也不会实际依赖除本身的字节之外的任何内存。当我们将它的每一位转移给 num2 时,其实已经为 num1 制作了一个完全独立的副本。

   移动一个值会使移动的源变成未初始化状态。不过,尽管将 string1 视为未初始化变量确实符合其基本意图,但以这种方式对待 num1 毫无意义,继续使用 num1 也不会造成任何问题。移动在这里并无好处,反而会造成不便。

   之前我们谨慎地说过,大多数类型会被移动,现在该谈谈例外情况了,即那些被 Rust 指定成 Copy 类型的类型。对 Copy 类型的值进行赋值会复制这个值,而不会移动它。赋值的源仍会保持已初始化和可用状态,并且具有与之前相同的值。把 Copy 类型传给函数和构造器的行为也是如此。

   标准的 Copy 类型包括所有机器整数类型和浮点数类型、char 类型和 bool 类型,以及某些其他类型。Copy 类型的元组或固定大小的数组本身也是 Copy 类型。

   只有那些可以通过简单地复制位来复制其值的类型才能作为 Copy 类型。前面解释过,String 不是 Copy 类型,因为它拥有从堆中分配的缓冲区。出于类似的原因,Box 也不是 Copy 类型,因为它拥有从堆中分配的引用目标。代表操作系统文件句柄的 File 类型不是 Copy 类型,因为复制这样的值需要向操作系统申请另一个文件句柄。类似地,MutexGuard 类型表示一个互斥锁,它也不是 Copy 类型:复制这种类型毫无意义,因为每次只能有一个线程持有互斥锁。

   根据经验,任何在丢弃值时需要做一些特殊操作的类型都不能是 Copy 类型:Vec 需要释放自身元素、File 需要关闭自身文件句柄、MutexGuard 需要解锁自身互斥锁,等等。对这些类型进行逐位复制会让我们无法弄清哪个值该对原始资源负责。

那么自定义类型呢?默认情况下,struct 类型和 enum 类型不是 Copy 类型:

struct Label { number: u32 }

fn print(l: Label) { println!("STAMP: {}", l.number); }
let l = Label { number: 3 };
print(l);
println!("My label number is: {}", l.number);

以上代码这无法编译。Rust 会报错,可以自己实际实验一番

   由于 Label 不是 Copy 类型,因此将它传给 print 会将值的所有权转移给 print 函数,然后在返回之前将其丢弃。这样做显然是愚蠢的,Label 中只有一个 u32,因此没有理由在将 l 传给 print 时移动这个值。

   但是用户定义的类型不是 Copy 类型这一点只是默认情况而已。如果此结构体的所有字段本身都是 Copy 类型,那么也可以通过将属性 #[derive(Copy, Clone)] 放置在此定义之上来创建 Copy 类型,如下所示:

#[derive(Copy, Clone)]
struct Label { number: u32 }

   经过此项更改,前面的代码可以顺利编译了。但是,如果试图在一个其字段不全是 Copy 类型的结构体上这样做,则仍然行不通。假设要编译如下代码,那么就会引发如下错误:

#[derive(Copy, Clone)]
struct StringLabel { name: String }

为什么符合条件的用户定义类型不能自动成为 Copy 类型呢?

   这是因为类型是否为 Copy 对于在代码中使用它的方式有着重大影响:Copy 类型更灵活,因为赋值和相关操作不会把原始值变成未初始化状态。但对类型的实现者而言,情况恰恰相反:Copy 类型可以包含的类型非常有限,而非 Copy 类型可以在堆上分配内存并拥有其他种类的资源。因此,创建一个 Copy 类型代表着实现者的郑重承诺:如果以后确有必要将其改为非 Copy 类型,则使用它的大部分代码可能需要进行调整。

   虽然 C++ 允许重载赋值运算符以及定义专门的复制构造函数和移动构造函数,但 Rust 并不允许这种自定义行为。在 Rust 中,每次移动都是字节级的一对一浅拷贝,并让源变成未初始化状态。复制也是如此,但会保留源的初始化状态。这确实意味着 C++ 类可以提供 Rust 类型所无法提供的便捷接口,比如可以在看似普通的代码中隐式调整引用计数、把昂贵的复制操作留待以后进行,或使用另一些复杂的实现技巧。

   但这种灵活性的代价是,作为一门语言,C++ 的基本操作(比如赋值、传参和从函数返回值)变得更难预测。例如,本章的前半部分展示过在 C++ 中将一个变量赋值给另一个变量时可能需要任意数量的内存和处理器时间。Rust 的一个原则是:各种开销对程序员来说应该是显而易见的。基本操作必须保持简单,而潜在的昂贵操作应该是显式的,比如前面例子中对 clone 的调用就是在对向量及其包含的字符串进行深拷贝。

1.4 Rc 与 Arc:共享所有权

   尽管在典型的 Rust 代码中大多数值会有唯一的拥有者,但在某些情况下,很难为每个值都找到具有所需生命周期的单个拥有者,你会希望某个值只要存续到每个人都用完它就好。对于这些情况,Rust 提供了引用计数指针类型 Rc 和 Arc [Arc 是原子引用计数(atomic reference count) 的缩写 ]。正如你对 Rust 的期待一样,这些类型用起来完全安全:你不会忘记调整引用计数,不会创建 Rust 无法注意到的指向引用目标的其他指针,也不会偶遇那些常与 C++ 中的引用计数指针如影随形的各种问题。

   Rc 类型和 Arc 类型非常相似,它们之间唯一的区别是 Arc 可以安全地在线程之间直接共享,而普通 Rc 会使用更快的非线程安全代码来更新其引用计数。如果不需要在线程之间共享指针,则没有理由为 Arc 的性能损失“埋单”,因此应该使用 Rc,Rust 能防止你无意间跨越线程边界传递它。这两种类型在其他方面都是等效的,所以本节的其余部分只会讨论 Rc。

使用 Rc 在 Rust 中获得引用计数来管理值的生命周期类似的效果:

use std::rc::Rc;

// Rust能推断出所有这些类型,这里写出它们只是为了讲解时清晰
let s: Rc<String> = Rc::new("shirataki".to_string());
let t: Rc<String> = s.clone();
let u: Rc<String> = s.clone();

   对于任意类型 T,Rc 值是指向附带引用计数的在堆上分配的 T 型指针。克隆一个 Rc 值并不会复制 T,相反,它只会创建另一个指向它的指针并递增引用计数。所以前面的代码在内存中会生成图所示的结果。

在这里插入图片描述
   这 3 个 Rc < String> 指针指向了同一个内存块,其中包含引用计数和 String 本身的空间。通常的所有权规则适用于 Rc 指针本身,当丢弃最后一个现有 Rc 时,Rust 也会丢弃 String。

可以直接在 Rc 上使用 String 的任何常用方法:

assert!(s.contains("shira"));
assert_eq!(t.find("taki"), Some(5));
println!("{} are quite chewy, almost bouncy, but lack flavor", u);

Rc 指针拥有的值是不可变的。如果你试图将一些文本添加到字符串的末尾,那么 Rust 会拒绝

s.push_str(" noodles");

   Rust 的内存和线程安全保证的基石是:确保不会有任何值是既共享又可变的。Rust 假定 Rc 指针的引用目标通常都可以共享,因此就不能是可变的。

为什么这个限制很重要?

   使用引用计数管理内存的一个众所周知的问题是,如果有两个引用计数的值是相互指向的,那么其中一个值就会让另一个值的引用计数保持在 0 以上,因此这些值将永远没机会释放,如图所示。

在这里插入图片描述

   以这种方式在 Rust 中造成值的泄漏也是有可能的,但这种情况非常少见。只要不在某个时刻让旧值指向新值,就无法建立循环。这显然要求旧值是可变的。由于 Rc 指针会保证其引用目标不可变,因此通常不可能建立这种循环引用。但是,Rust 确实提供了创建其他不可变值中的可变部分的方法,这称为内部可变性。如果将这些技术与 Rc 指针结合使用,则确实可以建立循环并造成内存泄漏。

移动和引用计数指针是缓解所有权树严格性问题的两种途径。

2.引用

Rust 还有一种名为引用(reference)的非拥有型指针,这种指针对引用目标的生命周期毫无影响。

   引用的生命周期绝不能超出其引用目标。你的代码必须遵循这样的规则,即任何引用的生命周期都不可能超出它指向的值。为了强调这一点,Rust 把创建对某个值的引用的操作称为借用(borrow)那个值:凡是借用,终须归还。

   本章将介绍引用在 Rust 中的工作方式,我们会展开讲解引用、函数和自定义类型是如何通过包含生命周期信息来确保它们被安全使用的,并阐明这些努力为何能在编译期就避免一些常见类别的缺陷,而不必在运行期付出性能方面的代价。

2.1 对值的引用

引用能让你在不影响其所有权的情况下访问值。引用分为以下两种。

   共享引用允许你读取但不能修改其引用目标。但是,你可以根据需要同时拥有任意数量的对特定值的共享引用。表达式 &e 会产生对 e 值的共享引用,如果 e 的类型为 T,那么 &e 的类型就是 &T。共享引用是 Copy 类型。

   可变引用允许你读取和修改值。但是,一旦一个值拥有了可变引用,就无法再对该值创建其他任何种类的引用了。表达式 &mut e 会产生一个对 e 值的可变引用,可以将其类型写成 &mut T。可变引用不是 Copy 类型。

2.2 Rust 引用

在 Rust 中,引用是通过 & 运算符显式创建的,同时要用 * 运算符显式解引用:

// 从这里开始回到Rust代码
let x = 10;
let r = &x;            // &x是对x的共享引用
assert!(*r == 10);     // 对r显式解引用

要创建可变引用,可以使用 &mut 运算符:

let mut y = 32;
let m = &mut y;        // &muty是对y的可变引用
*m += 32;              // 对m显式解引用,以设置y的值
assert!(*m == 64);     // 来看看y的新值

也许你还记得,当我们修复 show 函数以通过引用而非值来获取艺术家表格时,并未使用过 * 运算符。这是为什么呢?由于引用在 Rust 中随处可见,因此 . 运算符就会按需对其左操作数隐式解引用:

struct Anime { name: &'static str, bechdel_pass: bool }
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");

// 与上一句等效,但把解引用过程显式地写了出来
assert_eq!((*anime_ref).name, "Aria: The Animation");

show 函数中使用的 println! 宏会展开成使用 . 运算符的代码,因此它也能利用这种隐式解引用的方式。在进行方法调用时,. 运算符也可以根据需要隐式借用对其左操作数的引用。例如,Vec 的 sort 方法就要求参数是对向量的可变引用,因此这两个调用是等效的:

let mut v = vec![1973, 1968];
v.sort();           // 隐式借用对v的可变引用
(&mut v).sort();    // 等效,但是更烦琐

在 Rust 中要使用 & 运算符和 * 运算符来创建引用(借用)和追踪引用(解引用),不过 . 运算符不需要做这种转换,它会隐式借用和解引用。

对引用变量赋值

把引用赋值给某个引用变量会让该变量指向新的地方:

let x = 10;
let y = 20;
let mut r = &x;
if b { r = &y; }
assert!(*r == 10 || *r == 20);

引用 r 最初指向 x。但如果 b 为 true,则代码会把它改为指向 y,如图 所示。

在这里插入图片描述
现在 r 当然会指向 y,因为我们在其中存储了 &y。

对引用进行引用
Rust 允许对引用进行引用:

struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;

. 运算符会追踪尽可能多层次的引用来找到它的目标:

assert_eq!(rrr.y, 729);

在内存中,引用的排列方式如图 所示。

在这里插入图片描述在这里,表达式 rrr.y 根据 rrr 的类型的指引遍历了 3 层引用才取到相应 Point 的 y 字段。

比较引用

就像 . 运算符一样,Rust 的比较运算符也能“看穿”任意数量的引用:

let x = 10;
let y = 10;

let rx = &x;
let ry = &y;

let rrx = &rx;
let rry = &ry;

assert!(rrx <= rry);
assert!(rrx == rry);

   虽然 rrx 和 rry 指向的是不同的值(rx 和 ry),这里的断言最终仍然会成功,因为 == 运算符会追踪所有引用并对它们的最终目标 x 和 y 进行比较。这几乎总是你期望的行为,尤其是在编写泛型函数时。如果你真想知道两个引用是否指向同一块内存,可以使用 std::ptr::eq,它会将两者作为地址进行比较:

assert!(rx == ry);              // 它们引用的目标值相等
assert!(!std::ptr::eq(rx, ry)); // 但所占据的地址(自身的值)不同

但要注意,比较运算符的操作数(包括引用型操作数)必须具有完全相同的类型。

assert!(rx == rrx);    // 错误:`&i32`与`&&i32`的类型不匹配
assert!(rx == *rrx);   // 这样没问题

引用永不为空
   Rust 的引用永远都不会为空。与 C 的 NULL 或 C++ 的 nullptr 类似的东西是不存在的。引用没有默认初始值(在初始化之前不能使用任何变量,无论其类型如何),并且 Rust 不会将整数转换为引用(在 unsafe 代码外)。因此,不能将 0 转换成引用。C 代码和 C++ 代码通常会使用空指针来指示值的缺失:当可用内存充足时,malloc 函数会返回指向新内存块的指针,否则会返回 nullptr。在 Rust 中,如果需要用一个值来表示对某个“可能不存在”事物的引用,请使用类型 Option<&T>。在机器码级别,Rust 会将 None 表示为空指针,将 Some® 表示为非零地址(其中 r 是 &T 型的值),因此 Option<&T> 与 C 或 C++ 中的可空指针一样高效,但更安全:它的类型要求你在使用之前必须检查它是否为 None。

借用任意表达式结果值的引用
Rust 允许借用任意种类的表达式结果值的引用:

fn factorial(n: usize) -> usize {
    (1..n+1).product()
}
let r = &factorial(6);
// 数学运算符可以“看穿”一层引用
assert_eq!(r + &1009, 1729);

   在这种情况下,Rust 会创建一个匿名变量来保存此表达式的值,并让该引用指向它。这个匿名变量的生命周期取决于你对引用做了什么。

   在 let 语句中,如果立即将引用赋值给某个变量(或者使其成为立即被赋值的某个结构体或数组的一部分),那么 Rust 就会让匿名变量存在于 let 初始化此变量期间。在前面的示例中,Rust 就会对 r 的引用目标这样做。
否则,匿名变量会一直存续到所属封闭语句块的末尾。在我们的示例中,为保存 1009 而创建的匿名变量只会存续到 assert_eq! 语句的末尾。

   Rust 永远不会让你写出可能生成悬空引用的代码。只要引用可能在匿名变量的生命周期之外被使用,Rust 就一定会在编译期间报告问题,然后你就可以通过将引用保存在具有适当生命周期的命名变量中来修复代码。

对切片和特型对象的引用

   Rust 还包括两种胖指针,即携带某个值地址的双字值,以及要正确使用该值所需的某些额外信息。对切片的引用就是一个胖指针,携带着此切片的起始地址及其长度。第 3 章详细讲解过切片。Rust 的另一种胖指针是特型对象,即对实现了指定特型的值的引用。特型对象会携带一个值的地址和指向适用于该值的特型实现的指针,以便调用特型的方法。11.1.1 节会详细介绍特型对象。除了会携带这些额外数据,切片和特型对象引用的行为与本章中已展示过的其他引用是一样的:它们并不拥有自己的引用目标、它们的生命周期也不允许超出它们的引用目标、它们可能是可变的或共享的,等等。

2.3 引用安全

借用局部变量

将引用作为函数参数

把引用传给函数

返回引用

包含引用的结构体

不同的生命周期参数

省略生命周期参数

2.4 共享与可变

本书讨论的都是 Rust 如何确保不会有任何引用指向超出作用域的变量。但是还有其他方法可能引入悬空指针。下面是一个简单的例子:

let v = vec![4, 8, 19, 27, 34, 10];
let r = &v;
let aside = v;  // 把向量转移给aside
r[0];           // 错误:这里所用的`v`此刻是未初始化状态

对 aside 的赋值会移动向量、让 v 回到未初始化状态,并将 r 变为悬空指针,如图 所示(对已移除去的向量的引用)。

在这里插入图片描述   尽管 v 在 r 的整个生命周期中都处于作用域内部,但这里的问题是 v 的值已经移动到别处,导致 v 成了未初始化状态,而 r 仍然在引用它。当然,Rust 会捕获错误:

error: cannot move out of `v` because it is borrowed

   在共享引用的整个生命周期中,它引用的目标会保持只读状态,即不能对引用目标赋值或将值移动到别处。在上述代码中,r 的生命周期内发生了移动向量的操作,Rust 当然要拒绝。如果按如下所示更改程序,就没问题了:

let v = vec![4, 8, 19, 27, 34, 10];
{
    let r = &v;
    r[0];       // 正确:向量仍然在那里
}
let aside = v;

   在这个版本中,r 作用域范围更小,在把 v 转移给 aside 之前,r 的生命周期就结束了,因此一切正常。下面是另一种制造混乱的方式。假设我们随手写了一个函数,它使用切片的元素来扩展某个向量:

fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
    for elt in slice {
        vec.push(*elt);
    }
}

   这是标准库中向量的 extend_from_slice 方法的一个不太灵活(并且优化程度较低)的版本。可以用它从其他向量或数组的切片中构建一个向量:

let mut wave = Vec::new();
let head = vec![0.0, 1.0];
let tail = [0.0, -1.0];
extend(&mut wave, &head);   // 使用另一个向量扩展`wave`
extend(&mut wave, &tail);   // 使用数组扩展`wave`
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0]);

我们在这里建立了一个正弦波周期。如果想添加另一个周期,那么可以把向量追加到其自身吗?

extend(&mut wave, &wave);
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0,
                      0.0, 1.0, 0.0, -1.0]);

   在往向量中添加元素时,如果它的缓冲区已满,那么就必须分配一个具有更多空间的新缓冲区。假设开始时 wave 有 4 个元素的空间,那么当 extend 尝试添加第五个元素时就必须分配更大的缓冲区。内存最终如图 5-8 所示。extend 函数的 vec 参数借用了 wave(由调用者拥有),而 wave 为自己分配了一个新的缓冲区,其中有 8 个元素的空间。但是 slice 仍然指向旧的 4 元素缓冲区,该缓冲区已经被丢弃了。

在这里插入图片描述
   这种问题并不是 Rust 独有的:在许多语言中,在指向集合的同时修改集合要加倍小心。在 C++ 中,std::vector 规范会告诫你“重新分配向量缓冲区会令指向序列中各个元素的所有引用、指针和迭代器失效”。Java 对修改 java.util.Hashtable 对象也有类似的说法。如果在创建迭代器后的任何时间以任何方法(迭代器自身的 remove 方法除外)修改了 Hashtable 的结构,那么迭代器都将抛出 ConcurrentModificationException 异常。这类错误特别难以调试,因为它只会偶尔发生。在测试中,向量可能总是恰好有足够的空间,缓冲区可能永远都不会重新分配,于是这个问题可能永远都没人发现。然而,Rust 会在编译期报告调用 extend 有问题:

error: cannot borrow `wave` as immutable because it is also borrowed as mutable

   换句话说,我们既可以借用向量的可变引用,也可以借用其元素的共享引用,但这两种引用的生命周期不能重叠。在这个例子中,这两种引用的生命周期都包含着对 extend 的调用,出现了重叠,因此 Rust 会拒绝执行这段代码。这些错误都源于违反了 Rust 的“可变与共享”规则。共享访问是只读访问。共享引用借用的值是只读的。在共享引用的整个生命周期中,无论是它的引用目标,还是可从该引用目标间接访问的任何值,都不能被任何代码改变。这种结构中不能存在对任何内容的有效可变引用,其拥有者应保持只读状态,等等。值完全冻结了。可变访问是独占访问。可变引用借用的值只能通过该引用访问。在可变引用的整个生命周期中,无论是它的引用目标,还是该引用目标间接访问的任何目标,都没有任何其他路径可访问。对可变引用来说,唯一能和自己的生命周期重叠的引用就是从可变引用本身借出的引用。

   Rust 报告说 extend 示例违反了第二条规则:因为我们借用了对 wave 的可变引用,所以该可变引用必须是抵达向量或其元素的唯一方式。而对切片的共享引用本身是抵达这些元素的另一种方式,这违反了第二条规则。但是 Rust 也可以将我们的错误视为违反了第一条规则:因为我们借用了对 wave 元素的共享引用,所以这些元素和 Vec 本身都是只读的。不能对只读值借用出可变引用。每种引用都会影响到我们可以对“到引用目标从属路径上的值”以及“从引用目标可间接访问的值”所能执行的操作,如图 5-9 所示。

在这里插入图片描述   请注意,在这两种情况下,指向引用目标的所有权路径在此引用的生命周期内都无法更改。对于共享借用,这条路径是只读的;对于可变借用,这条路径是完全不可访问的。所以程序无法做出任何会使该引用无效的操作。可以将这些原则分解为一些最简单的示例:

let mut x = 10;
let r1 = &x;
let r2 = &x;     // 正确:允许多个共享借用
x += 10;         // 错误:不能赋值给`x`,因为它已被借出
let m = &mut x;  // 错误:不能把`x`借入为可变引用,因为
                 // 它涵盖在已借出的不可变引用的生命周期内
println!("{}, {}, {}", r1, r2, m); // 这些引用是在这里使用的,所以它们
                                   // 的生命周期至少要存续这么长
let mut y = 20;
let m1 = &mut y;
let m2 = &mut y;  // 错误:不能多次借入为可变引用
let z = y;        // 错误:不能使用`y`,因为它涵盖在已借出的可变引用的生命周期内
println!("{}, {}, {}", m1, m2, z); // 在这里使用这些引用

可以从共享引用中重新借入共享引用:

let mut w = (107, 109);
let r = &w;
let r0 = &r.0;         // 正确:把共享引用重新借入为共享引用
let m1 = &mut r.1;     // 错误:不能把共享引用重新借入为可变
println!("{}", r0);    // 在这里使用r0

可以从可变引用中重新借入可变引用:

let mut v = (136, 139);
let m = &mut v;
let m0 = &mut m.0;      // 正确: 从可变引用中借入可变引用
*m0 = 137;
let r1 = &m.1;          // 正确: 从可变引用中借入共享引用,并且不能和m0重叠
v.1;                    // 错误:禁止通过其他路径访问
println!("{}", r1);     // 可以在这里使用r1

   这些限制非常严格。回过头来看看我们尝试调用 extend(&mut wave, &wave) 的地方,没有什么快速而简便的方法来修复代码,以使其按照我们想要的方式工作。Rust 中到处都在应用这些规则:如果要借用对 HashMap 中键的共享引用,那么在共享引用的生命周期结束之前就不能再借入对 HashMap 的可变引用。但这么做有充分的理由:要为集合设计出“支持不受限制地在迭代期间修改”的能力是非常困难的,而且往往会导致无法简单高效地实现这些集合。Java 的 Hashtable 和 C++ 的 vector 就不支持这种访问方式,Python 的字典和 JavaScript 的对象甚至都不曾定义过这种访问方式。JavaScript 中的其他集合类型固然可以做到,不过需要更繁重的实现。C++ 的 std::map 承诺插入新条目不会让指向此映射表中其他条目的指针失效,但做出这一承诺的代价是该标准无法提供像 Rust 的 BTreeMap 这样更高效的缓存设计方案,因为后者会在树的每个节点中存储多个条目。下面是通过上述规则捕获各种错误的另一个例子。考虑以下 C++ 代码,它用于管理文件描述符。为了简单起见,这里只展示一个构造函数和复制赋值运算符,并会省略错误处理代码:

struct File {
    int descriptor;
    File(int d) : descriptor(d) { }
    File& operator=(const File &rhs) {
    close(descriptor);
    descriptor = dup(rhs.descriptor);
    return *this;
    }
};

这个赋值运算符很简单,但在下面这种情况下会执行失败:

File f(open("foo.txt", ...));
...
f = f;

   如果将一个 File 赋值给它自己,那么 rhs 和 *this 就是同一个对象,所以 operator= 会关闭它要传给 dup 的文件描述符。也就是说,我们销毁了正打算复制的那份资源。在 Rust 中,类似的代码如下所示:

struct File {
    descriptor: i32
}

fn new_file(d: i32) -> File {
    File { descriptor: d }
}

fn clone_from(this: &mut File, rhs: &File) {
    close(this.descriptor);
    this.descriptor = dup(rhs.descriptor);
}

如果编写使用了 File 的 Rust 代码,就会得到如下内容:

let mut f = new_file(open("foo.txt", ...));
...
clone_from(&mut f, &f);

当然,Rust 干脆拒绝编译这段代码:

error: cannot borrow `f` as immutable because it is also borrowed as mutable

   以上错误看起来很熟悉。事实证明,这里的两个经典 C++ 错误(无法处理自赋值和使用无效迭代器)本质上是同一种错误。在这两种情况下,代码都以为自己正在修改一个值,同时在引用另一个值,但实际上两者是同一个值。如果你不小心让调用 memcpy 或 strcpy 的源和目标在 C 或 C++ 中重叠,则可能会带来另一种错误。通过要求可变访问必须是独占的,Rust 避免了一大类日常错误。在编写并发代码时,共享引用和可变引用的互斥性确实证明了其价值。只有当某些值既可变又要在线程之间共享时,才可能出现数据竞争,而这正是 Rust 的引用规则所要消除的。一个用 Rust 编写的并发程序,只要避免使用 unsafe 代码,就可以在构造之初就避免产生数据竞争。第 19 章在讨论并发时会更详细地对此进行介绍。总而言之,与大多数其他语言相比,并发在 Rust 中更容易使用。

Rust 的共享引用与 C 的 const 指针

   Rust 的共享引用似乎与 C 和 C++ 中指向 const 值的指针非常相似。然而,Rust 中共享引用的规则要严格得多。例如,考虑以下 C 代码:

int x = 42;              // int变量,不是常量
const int *p = &x;       // 指向const int的指针
assert(*p == 42);
x++;                     // 直接修改变量
assert(*p == 43);        //“常量”指向的值发生了变化

   p 是 const int * 这一事实意味着不能通过 p 本身修改它的引用目标,也就是说,禁止使用 (*p)++。但是可以直接通过 x 获取引用目标,x 不是 const,能以这种方式更改其值。C 家族的 const 关键字自有其用处,但与“常量”无关。在 Rust 中,共享引用禁止对其引用目标进行任何修改,直到其生命周期结束:

let mut x = 42;          // 非常量型i32变量
let p = &x;              // 到i32的共享引用
assert_eq!(*p, 42);
x += 1;                  // 错误:不能对x赋值,因为它已被借出
assert_eq!(*p, 42);      // 如果赋值成功,那么这应该是true

   为了保证一个值是常量,需要追踪该值的所有可能路径,并确保它们要么不允许修改,要么根本不能使用。C 和 C++ 的指针不受限制,编译器无法对此进行检查。Rust 的引用总是与特定的生命周期相关联,因此可以在编译期检查它们。

3.特型与泛型

简介

本章展示特型的用法、工作原理,以及如何定义你自己的特型。

标准库提供的公共特型。之后的闭包、迭代器、输入 / 输出和并发。特型和泛型在所有这些主题中都扮演着核心角色。

Rust 通过两个相关联的特性来支持多态:特型和泛型。

特型是 Rust 体系中的接口或抽象基类。

为什么向类型添加特型不需要额外的内存?
如何在不需要虚方法调用开销的情况下使用特型?

泛型是 Rust 中多态的另一种形式。

泛型和特型紧密相关:泛型函数会在限界中使用特型来阐明它能针对哪些类型的参数进行调用。

3.1 使用特型

特型代表着一种能力,即一个类型能做什么。

  • 实现了 std::io::Write 的值能写出一些字节。
  • 实现了 std::iter::Iterator 的值能生成一系列值。
  • 实现了 std::clone::Clone 的值能在内存中克隆自身。
  • 实现了 std::fmt::Debug 的值能用带有 {:?} 格式说明符的 println!() 打印出来。

特型方法类似于虚方法。

3.2 特型对象

在 Rust 中使用特型编写多态代码有两种方法:特型对象和泛型。

在 Rust 中,引用是显式的:

let mut buf: Vec<u8> = vec![];
let writer: &mut dyn Write = &mut buf; // 正确

对特型类型(如 writer)的引用叫作特型对象。

特型对象的内存布局

在内存中,特型对象是一个胖指针,由指向值的指针和指向表示该值类型的虚表的指针组成。

C++ 也有这种运行期类型信息,叫作虚表或 vtable。

在这里插入图片描述   在 C++ 中,虚表指针或 vptr 是作为结构体的一部分存储的,而 Rust 使用的是胖指针方案。结构体本身只包含自己的字段。这样一来,每个结构体就可以实现几十个特型而不必包含几十个 vptr 了。甚至连 i32 这样大小不足以容纳 vptr 的类型都可以实现特型。

3.3 泛型函数与类型参数

4.实用工具特型

4.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 来保存它的文本,因此 String 不需要自己实现 Drop,它会让 Vec 负责释放这些字符。同样的原则也适用于 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?”);

   根据 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(_x: T) { }

   换句话说,它会按值接受参数,从调用者那里获得所有权,然后什么也不做。当 _x 超出作用域时,Rust 自然会丢弃它的值,这跟对任何其他变量的操作一样。

4.2 Sized

   固定大小类型是指其每个值在内存中都有相同大小的类型。Rust 中的几乎所有类型都是固定大小的,比如每个 u64 占用 8 字节,每个 (f32, f32, f32) 元组占用 12 字节。甚至枚举也是有大小的,也就是说,无论实际存在的是哪个变体,枚举总会占据足够的空间来容纳其最大的变体。尽管 Vec 拥有一个大小可变的堆分配缓冲区,但 Vec 值本身是指向“缓冲区、容量和长度”的指针,因此 Vec 也是一个固定大小类型。

   所有固定大小类型都实现了 std::marker::Sized 特型,该特型没有方法或关联类型。Rust 自动为所有适用的类型实现了 std::marker::Sized 特型,你不能自己实现它。Sized 的唯一用途是作为类型变量的限界:像 T: Sized 这样的限界要求 T 必须是在编译期已知的类型。由于 Rust 语言本身会使用这种类型的特型为具有某些特征的类型打上标记,因此我们将其称为标记特型。

   然而,Rust 也有一些无固定大小类型,它们的值大小不尽相同。例如,字符串切片类型 str(注意没有 &)就是无固定大小的。字符串字面量 “diminutive” 和 “big” 是对占用了 10 字节和 3 字节的 str 切片的引用,两者都展示在图 13-1 中。像 [T](同样没有 &)这样的数组切片类型也是无固定大小的,即像 &[u8] 这样的共享引用可以指向任意大小的 [u8] 切片。因为 str 类型和 [T] 类型都表示不定大小的值集,所以它们是无固定大小类型。

在这里插入图片描述
   Rust 中另一种常见的无固定大小类型是 dyn 类型,它是特型对象的引用目标。正如我们在 11.1.1 节中所解释的那样,特型对象是指向实现了给定特型的某个值的指针。例如,类型 &dyn std::io::Write 和 Box 是指向实现了 Write 特型的某个值的指针。引用目标可能是文件、网络套接字,或某种实现了 Write 的自定义类型。由于实现了 Write 的类型集是开放式的,因此 dyn Write 作为一个类型也是无固定大小的,也就是说它的值可以有各种大小。Rust 不能将无固定大小的值存储在变量中或将它们作为参数传递。你只能通过像 &str 或 Box 这样的本身是固定大小的指针来处理它们。如图 13-1 所示,指向无固定大小值的指针始终是一个胖指针,宽度为两个机器字:指向切片的指针带有切片的长度,特型对象带有指向方法实现的虚表的指针。特型对象和指向切片的指针在结构上很像。这两种类型都缺乏某种在使用它们时必要的信息。换句话说,你无法在不知道其长度的情况下对 [u8] 进行索引,也无法在不知道该对某个值使用 Write 的哪个具体实现的情况下调用 Box 的方法。对于这两种类型,胖指针都会补齐类型中缺少的信息——它携带着长度或虚表指针。既然欠缺静态信息,那就用动态信息来弥补。

   由于无固定大小类型处处受限,因此大多数泛型类型变量应当被限制为固定大小的 Sized 类型。事实上,鉴于这种情况的普遍性,Sized 已经成为 Rust 中的隐式默认值:如果你写 struct S { … },那么 Rust 会将其理解为 struct S { … }。如果你不想以这种方式约束 T,就必须将其明确地排除,写成 struct S { … }。?Sized 语法专用于这种情况,意思是“不要求固定大小的”。如果你写 struct S { b: Box },那么 Rust 将允许写成 S 和 S,这样这两个 Box 就变成了胖指针,而不像 S 和 S 的 Box 那样只是普通指针。尽管存在一些限制,但无固定大小类型能让 Rust 的类型系统工作得更顺畅。阅读标准库文档时,你偶尔会遇到类型变量上的 ?Sized 限界,这几乎总是表明“给定的类型只能通过指针访问”,并能让其关联的代码与切片对象和特型对象以及普通值一起使用。当类型变量具有 ?Sized 限界时,人们认为它的大小不确定,既可能是固定大小,也可能不是。除了切片对象和特型对象,还有另一种无固定大小类型。结构体类型的最后一个字段(而且只能是最后一个)可以是无固定大小的,并且这样的结构体本身也是无固定大小的。例如,Rc 引用计数指针的内部实现是指向私有类型 RcBox 的指针,后者把引用计数和 T 保存在一起。下面是 RcBox 的简化定义:

struct RcBox<T: ?Sized> {
ref_count: usize,
value: T,
}

   Rc 是引用计数指针,其中的 value 字段是 Rc 对其进行引用计数的 T 类型。Rc 会解引用成指向 value 字段的指针。ref_count 字段会保存其引用计数。真正的 RcBox 只是标准库的一个实现细节,无法在外部使用。但假设我们正在使用前面这种定义,那么就可以将此 RcBox 与固定大小类型一起使用,比如 RcBox 的结果是一个固定大小的结构体类型。或者也可以将它与无固定大小类型一起使用,比如 RcBox(其中 Display 是可以通过 println! 之类的宏进行格式化的类型的特型),结果 RcBox 就成了无固定大小的结构体类型。不能直接构建 RcBox 值,而应该先创建一个普通的固定大小的 RcBox,并让其 value 类型实现 Display,比如 RcBox。然后 Rust 就会允许你将引用 &RcBox 转换为胖引用 &RcBox:

let boxed_lunch: RcBox = RcBox {
ref_count: 1,
value: “lunch”.to_string()
};

use std::fmt::Display;
let boxed_displayable: &RcBox = &boxed_lunch;

在将值传给函数时会发生隐式转换,这样你就可以将 &RcBox 传给需要 &RcBox 的函数:

fn display(boxed: &RcBox) {
println!(“For your enjoyment: {}”, &boxed.value);
}

display(&boxed_lunch);

这将生成以下输出。

For your enjoyment: lunch

4.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 不仅会复制此向量,还会复制它的每个 String 元素。这就是 Rust 不会自动克隆值,而是要求你进行显式方法调用的原因。像 Rc 和 Arc 这样的引用计数指针类型属于例外,即克隆其中任何一个都只会增加引用计数并为你返回一个新指针。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 等原始类型实现了 Clone,String、Vec 和 HashMap 等容器类型也实现了 Clone。而那些无法合理复制的类型(如 std::sync::Mutex)则没有实现 Clone。像 std::fs::File 这样的类型虽然可以复制,但如果操作系统无法提供必要的资源,则复制可能会失败。这些类型也没有实现 Clone,因为 clone 必须是不会失败的。作为替代,std::fs::File 提供了一个 try_clone 方法,该方法会返回一个 std::io::Result 值,用以报告失败信息。

4.4 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 节曾详细解释过这些因素。

4.5 Deref 与 DerefMut

通过实现 std::ops::Deref 特型和 std::ops::DerefMut 特型,可以指定像 * 和 . 这样的解引用运算符在你的类型上的行为。像 Box 和 Rc 这样的指针类型就实现了这些特型,因此它们可以像 Rust 的内置指针类型那样用。如果你有一个 Box 型的值 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;
}

deref 方法会接受 &Self 引用并返回 &Self::Target 引用,而 deref_mut 方法会接受 &mut Self 引用并返回 &mut Self::Target 引用。Target 应该是 Self 包含、拥有或引用的资源:对于 Box,其 Target 类型是 Complex。请注意 DerefMut 扩展了 Deref:如果可以解引用并修改某些资源,那么当然也可以借入对它的共享引用。由于这些方法会返回与 &self 生命周期相同的引用,因此只要返回的引用还存在,self 就会一直处于已借出状态。Deref 特型和 DerefMut 特型还扮演着另一个角色。由于 deref 会接受 &Self 引用并返回 &Self::Target 引用,因此 Rust 会利用这一点自动将前一种类型的引用转换为后一种类型的引用。换句话说,如果只要插入一个 deref 调用就能解决类型不匹配问题,那 Rust 就会插入它。实现 DerefMut 也可以为可变引用启用相应的转换。这些叫作隐式解引用:一种类型被“转换”成了另一种类型。尽管隐式解引用也可以通过显式的方式写出来,但隐式解引用使用起来更方便。

如果你有一个 Rc 型的值 r,并想对其调用 String::find,就可以简单地写成 r.find(‘?’),而不用写成 (*r).find(‘?’):这种方法调用会隐式借入 r,并将 &Rc 转换为 &String,因为 Rc 实现了 Deref。你可以对 String 值使用 split_at 之类的方法,虽然 split_at 是在 str 切片类型上定义的方法,但因为 String 实现了 Deref,所以可以这样写。String 不需要重新实现 str 的所有方法,因为可以将 &String 隐式转换为 &str。如果你有一个字节向量 v 并且想将它传给需要字节切片 &[u8] 的函数,就可以简单地将 &v 作为参数传递,因为 Vec 实现了 Deref。在必要的情况下,Rust 会连续应用多个隐式解引用。例如,使用前面提到的隐式转换,你可以将 split_at 直接应用于 Rc,因为 &Rc 解引用成了 &String,后者又解引用成了 &str,而 &str 具有 split_at 方法。假设你有以下类型:

struct Selector {
/// 在这个Selector中可用的元素
elements: Vec,

/// `elements`中“当前”(current)元素的索引
/// `Selector`的行为类似于指向当前元素的指针
current: usize

}

要让 Selector 的行为与文档型注释中声明的一致,就必须为该类型实现 Deref 和 DerefMut:
use std::ops::;

impl Deref for Selector {
type Target = T;
fn deref(&self) -> &T {
&self.elements[self.current]
}
}

impl DerefMut for Selector {
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 和 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);

4.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, HashSet)
= 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 以将 T 添加到集合中就可以。String 实现了 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(…, &params).unwrap();

这会调用 Default::default() 来创建一个 DrawParameters 值,该值会使用其所有字段的默认值进行初始化,然后使用结构体的 … 语法创建出一个更改了 line_width 字段和 point_size 字段的新值,最后就可以把它传给 target.draw 了。如果类型 T 实现了 Default,那么标准库就会自动为 Rc、Arc、Box、Cell、RefCell、Cow、Mutex 和 RwLock 实现 Default。例如,类型 Rc 的默认值就是一个指向类型 T 的默认值的 Rc。如果一个元组类型的所有元素类型都实现了 Default,那么该元组类型也同样会实现 Default,这个元组的默认值包含每个元素的默认值。Rust 不会为结构体类型隐式实现 Default,但是如果结构体的所有字段都实现了 Default,则可以使用 #[derive(Default)] 为此结构体自动实现 Default。

4.7 AsRef 与 AsMut

如果一个类型实现了 AsRef,那么就意味着你可以高效地从中借入 &T。AsMut 是 AsRef 针对可变引用的对应类型。它们的定义如下所示:
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}

trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}

例如,Vec 实现了 AsRef<[T]>,而 String 实现了 AsRef。还可以把 String 的内容借入为字节数组,因此 String 也实现了 AsRef<[u8]>。AsRef 通常用于让函数更灵活地接受其参数类型。例如,std::fs::File::open 函数的声明如下:

fn open<P: AsRef >(path: P) -> Result

标准库的所有文件系统访问函数都会以这种方式接受路径参数。对调用者来说,其效果类似于 C++ 中的重载函数,只不过 Rust 采用的是另一种方式来确定可接受的参数类型。但这还不是全部。字符串字面量是 &str,实现了 AsRef 的类型是 str,并没有 &。正如我们在 13.5 节中解释的那样,Rust 不会试图通过隐式解引用来满足类型变量限界,因此就算它们在这里也无济于事。幸运的是,标准库包含了其通用实现:

impl<'a, T, U> AsRef for &'a T
where T: AsRef,
T: ?Sized, U: ?Sized
{
fn as_ref(&self) -> &U {
(*self).as_ref()
}
}

换句话说,对于任意类型 T 和 U,只要满足 T: AsRef,就必然满足 &T: AsRef:只需追踪引用并像以前那样继续处理即可。特别是,如果满足 str: AsRef,那么也会满足 &str: AsRef。从某种意义上说,这是一种在检查类型变量的 AsRef 限界时获得受限隐式解引用的方法。你可能会认为,如果一个类型实现了 AsRef,那么它也应该实现 AsMut。但是,这在某些情况下是不合适的。例如,我们已经提到 String 实现了 AsRef<[u8]>,这是合理的,因为每个 String 肯定都有一个可以作为二进制数据访问的字节缓冲区。但是,String 要进一步保证这些字节是表示 Unicode 文本的一段格式良好的 UTF-8 编码,如果 String 实现了 AsMut<[u8]>,那么就会允许调用者将 String 的字节更改为他们想要的任何内容,这样你就不能再相信 String 一定是格式良好的 UTF-8 了。只有修改给定的 T 肯定不会违反此类型的不变性约束时,实现 AsMut 的类型才有意义。尽管 AsRef 和 AsMut 非常简单,但为引用转换提供标准的泛型特型可避免更专用的转换特型数量激增。只要能实现 AsRef,就要尽量避免定义自己的 AsFoo 特型。

4.8 Borrow 与 BorrowMut

std::borrow::Borrow 特型类似于 AsRef:如果一个类型实现了 Borrow,那么它的 borrow 方法就能高效地从自身借入一个 &T。但是 Borrow 施加了更多限制:只有当 &T 能通过与它借来的值相同的方式进行哈希和比较时,此类型才应实现 Borrow。(Rust 并不强制执行此限制,它只是记述了此特型的意图。)这使得 Borrow 在处理哈希表和树中的键或者处理因为某些原因要进行哈希或比较的值时非常有用。这在区分对 String 的借用时很重要,比如 String 实现了 AsRef、AsRef<[u8]> 和 AsRef,但这 3 种目标类型通常具有不一样的哈希值。只有 &str 切片才能保证像其等效的 String 一样进行哈希,因此 String 只实现了 Borrow。Borrow 的定义与 AsRef 的定义基本相同,只是名称变了:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}

Borrow 旨在解决具有泛型哈希表和其他关联集合类型的特定情况。假设你有一个 std::collections::HashMap,用于将字符串映射到数值。这个表的键是 String,每个条目都有一个键。在这个表中查找某个条目的方法的签名应该是什么呢?下面是第一次尝试。
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: Eq + Hash
{ … }
}

换句话说,只要可以借入一个条目的键充当 &Q,并且对生成的引用进行哈希和比较的方式与键本身一致,&Q 显然就是可接受的键类型。由于 String 实现了 Borrow 和 Borrow,因此最终版本的 get 允许按需传入 &String 型或 &str 型的 key。Vec 和 [T; N] 实现了 Borrow<[T]>。每个类似字符串的类型都能借入其相应的切片类型:String 实现了 Borrow、PathBuf 实现了 Borrow,等等。标准库中所有关联集合类型都使用 Borrow 来决定哪些类型可以传给它们的查找函数。标准库中包含一个通用实现,因此每个类型 T 都可以从自身借用:T: Borrow。这确保了在 HashMap 中查找条目时 &K 总是可接受的类型。为便于使用,每个 &mut T 类型也都实现了 Borrow,它会像往常一样返回一个共享引用 &T。这样你就可以给集合的查找函数传入可变引用,而不必重新借入共享引用,以模拟 Rust 通常会从可变引用到共享引用进行的隐式转换。BorrowMut 特型则类似于针对可变引用的 Borrow:

trait BorrowMut<Borrowed: ?Sized>: Borrow {
fn borrow_mut(&mut self) -> &mut Borrowed;
}

刚才讲过的对 Borrow 的要求同样适用于 BorrowMut。

4.9 From 与 Into

std::convert::From 特型和 std::convert::Into 特型表示类型转换,这种转换会接受一种类型的值并返回另一种类型的值。AsRef 特型和 AsMut 特型用于从一种类型借入另一种类型的引用,而 From 和 Into 会获取其参数的所有权,对其进行转换,然后将转换结果的所有权返回给调用者。From 和 Into 的定义是对称的:

trait Into: Sized {
fn into(self) -> T;
}

trait From: Sized {
fn from(other: T) -> Self;
}

标准库自动实现了从每种类型到自身的简单转换:每种类型 T 都实现了 From 和 Into。虽然这两个特型看起来只是为做同一件事提供了两种方式,但其实它们有不同的用途。你通常可以使用 Into 来让你的函数在接受参数时更加灵活。如果你写如下代码:

use std::net::Ipv4Addr;

fn ping(address: A) -> std::io::Result
where A: Into
{
let ipv4_address = address.into();

}

那么 ping 不仅可以接受 Ipv4Addr 作为参数,还可以接受 u32 或 [u8; 4] 数组,因为这些类型都恰好实现了 Into。(有时将 IPv4 地址视为单个 32 位值或 4 字节数组会很有用。)因为 ping 对 address 的唯一了解就是它要实现 Into,所以在调用 into 时无须指定想要的是哪种类型。因为只会存在一种有效类型,所以类型推断会替你补全它。

与 13.7 节中的 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

而 From 特型扮演着另一种角色。from 方法会充当泛型构造函数,用于从另一个值生成本类型的实例。例如,虽然 Ipv4Addr 有两个名为 from_array 和 from_u32 的方法,但 From 只是简单地实现了 From<[u8;4]> 和 From,于是我们就能这样写:

可以让类型推断找出适用于此的实现。给定适当的 From 实现,标准库会自动实现相应的 Into 特型。当你定义自己的类型时,如果它具有某些单参数构造函数,那么就应该将它们写成适当类型的 From 的实现,这样你就会自动获得相应的 Into 实现。因为转换方法 from 和 into 会接手它们的参数的所有权,所以此转换可以复用原始值的资源来构造出转换后的值。假设你写如下代码:

let text = “Beautiful Soup”.to_string();
let bytes: Vec = text.into();

String 的 Into> 的实现只是获取 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 实现了 From>,它能根据算法的要求对元素进行比较和重新排序。

通过在需要时自动将具体错误类型转换为一般错误类型,运算符 ? 可以使用 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 = Result<T, GenericError>;

fn parse_i32_bytes(b: &[u8]) -> GenericResult {
Ok(std::str::from_utf8(b)?.parse::()?)
}

与大多数错误类型一样,Utf8Error 和 ParseIntError 也实现了 Error 特型,标准库为我们提供了 From 的通用实现,用于将任何实现了 Error 的类型转换为 Box 类型,? 运算符会自动使用这种转换:

impl<'a, E: Error + Send + Sync + 'a> From
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

有很多选项可以处理这种情况。根据上下文的不同,“回绕型”转换可能比较合适。另外,像数字信号处理和控制系统这样的应用程序通常会使用“饱和型”转换,它会把比可能的最大值还要大的数值限制为最大值。

4.10 TryFrom 与 TryInto

由于转换的行为方式不够清晰,因此 Rust 没有为 i32 实现 From,也没有实现任何其他可能丢失信息的数值类型之间的转换,而是为 i32 实现了 TryFrom。TryFrom 和 TryInto 是 From 和 Into 的容错版“表亲”,这种转换同样是双向的,实现了 TryFrom 也就意味着实现了 TryInto。TryFrom 和 TryInto 的定义比 From 和 Into 稍微复杂一点儿。

pub trait TryFrom: Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto: 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 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 中关联多个类型。‘

4.11 ToOwned

给定一个引用,如果此类型实现了 std::clone::Clone,则生成其引用目标的拥有型副本的常用方法是调用 clone。但是当你想克隆一个 &str 或 &[i32] 时该怎么办呢?你想要的可能是 String 或 Vec,但 Clone 的定义不允许这样做:根据定义,克隆 &T 必须始终返回 T 类型的值,并且 str 和 [u8] 是无固定大小类型,它们甚至都不是函数所能返回的类型。std::borrow::ToOwned 特型提供了一种稍微宽松的方式来将引用转换为拥有型的值:

trait ToOwned {
type Owned: Borrow;
fn to_owned(&self) -> Self::Owned;
}

与必须精确返回 Self 类型的 clone 不同,to_owned 可以返回任何能让你从中借入 &Self 的类型:Owned 类型必须实现 Borrow。你可以从 Vec 借入 &[T],所以只要 T 实现了 Clone,[T] 就能实现 ToOwned>,这样就可以将切片的元素复制到向量中了。同样,str 实现了 ToOwned,Path 实现了 ToOwned,等等。

Borrow 与 ToOwned 的实际运用:谦卑 的 Cow

要想用好 Rust,就必然涉及对所有权问题的透彻思考,比如函数应该通过引用还是值接受参数。通常你可以任选一种方式,让参数的类型反映你的决定。但在某些情况下,在程序开始运行之前你无法决定是该借用还是该拥有,std::borrow::Cow 类型(用于“写入时克隆”,clone on write 的缩写)提供了一种兼顾两者的方式。std::borrow::Cow 的定义如下所示:

enum Cow<'a, B: ?Sized>
where B: ToOwned
{
Borrowed(&'a B),
Owned(::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::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()
}
}
}

上述代码使用了 Cow 的 Into 实现来构造出值。此 match 语句的大多数分支会返回 Cow::Borrowed 来引用静态分配的字符串。但是当我们得到一个 FileNotFound 变体时,会使用 format! 来构建包含给定文件名的消息。match 语句的这个分支会生成一个 Cow::Owned 值。如果 describe 的调用者不打算更改值,就可以直接把此 Cow 看作 &str:

println!(“Disaster has struck: {}”, describe(&error));

如果调用者确实需要一个拥有型的值,那么也能很容易地生成一个:

let mut log: Vec = Vec::new();

log.push(describe(&error).into_owned());

使用 Cow,describe 及其调用者可以把分配的时机推迟到确有必要的时候。

5.闭包

fn sort_cities(cities: &mut Vec<City>) {
    cities.sort_by_key(|city| -city.population);
}

|city| -city.population 就是闭包。它会接受一个参数 city 并返回 -city.population。Rust 会从闭包的使用方式中推断出其参数类型和返回类型。

闭包相关问题:
Rust 的闭包与匿名函数有何不同?
如何将闭包与标准库方法一起使用?
闭包如何“捕获”其作用域内的变量?
如何编写自己的以闭包作为参数的函数和方法?
何存储闭包供以后用作回调?
Rust 闭包是如何实现的,以及它们为什么比你预想的要快?

捕获变量

闭包可以使用属于其所在函数的数据:

/// 根据任何其他的统计标准排序
fn sort_by_statistic(cities: &mut Vec, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat));
}

借用值的闭包

首先,我们重复一下本节开头的例子:
/// 根据任何其他的统计标准排序
fn sort_by_statistic(cities: &mut Vec, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat));
}
在这种情况下,当 Rust 创建闭包时,会自动借入对 stat 的引用。这很合理,因为闭包引用了 stat,所以闭包必须包含对 stat 的引用。剩下的就简单了。闭包同样遵循第 5 章中讲过的关于借用和生命周期的规则。特别是,由于闭包中包含对 stat 的引用,因此 Rust 不会让它的生命周期超出 stat。因为闭包只会在排序期间使用,所以这个例子是适用的。简而言之,Rust 会使用生命周期而非垃圾回收来确保安全。Rust 的方式更快,因为即使是最快的垃圾回收器在分配内存时也会比把 stat 保存在栈上慢,本例中 Rust 就把 stat 保存在栈上。

“窃取”值的闭包
第二个例子比较棘手:
use std::thread;

fn start_sorting_thread(mut cities: Vec, stat: Statistic)
-> thread::JoinHandle<Vec>
{
let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };

thread::spawn(|| {
    cities.sort_by_key(key_fn);
    cities
})

}

这有点儿像我们的 JavaScript 示例所做的:thread::spawn 会接受一个闭包并在新的系统线程中调用它。请注意 || 是闭包的空参数列表。新线程会和调用者并行运行。当闭包返回时,新线程退出。(闭包的返回值会作为 JoinHandle 值发送回调用线程。)同样,闭包 key_fn 包含对 stat 的引用。但这一次,Rust 不能保证此引用的安全使用。因此 Rust 会拒绝编译这个程序:

error: closure may outlive the current function, but it borrows stat, which is owned by the current function

其实这里还有一个问题,因为 cities 也被不安全地共享了。简单来说,thread::spawn 创建的新线程无法保证在 cities 和 stat 被销毁之前在函数末尾完成其工作。这两个问题的解决方案是一样的:要求 Rust 将 cities 和 stat移动到使用它们的闭包中,而不是借入对它们的引用。

fn start_sorting_thread(mut cities: Vec, stat: Statistic)
-> thread::JoinHandle<Vec>
{
let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };

thread::spawn(move || {
    cities.sort_by_key(key_fn);
    cities
})

}

这里唯一的改动是在两个闭包之前都添加了 move 关键字。move 关键字会告诉 Rust,闭包并不是要借入它用到的变量,而是要“窃取”它们。第一个闭包 key_fn 取得了 stat 的所有权。第二个闭包则取得了 cities 和 key_fn 的所有权。因此,Rust 为闭包提供了两种从封闭作用域中获取数据的方法:移动和借用。实际上,关于移动和借用,闭包所遵循的正是第 4 章和第 5 章中已经介绍过的规则。下面举几个例子。就像 Rust 这门语言中的其他地方一样,如果闭包要移动可复制类型的值(如 i32),那么就会复制该值。因此,如果 Statistic 恰好是可复制类型,那么即使在创建了要使用 stat 的 move 闭包之后,我们仍可以继续使用 stat。不可复制类型的值(如 Vec)则确实会被移动,比如前面的代码就通过 move 闭包将 cities 转移给了新线程。在创建此闭包后,Rust 就不允许再通过 cities 访问它了。实际上,在闭包将 cities 移动之后,此代码就不需要再使用它了。但是,即使我们确实需要在此之后使用 cities,解决方法也很简单:可以要求 Rust 克隆 cities 并将副本存储在另一个变量中。闭包将只会“窃取”其中一个副本,即它所引用的那个副本。通过遵循 Rust 的严格规则,我们也收获颇丰,那就是线程安全。正是因为向量是被移动的,而不是跨线程共享的,所以我们知道旧线程肯定不会在新线程正在修改向量的时候释放它。

函数与闭包的类型

在本章中,函数和闭包都在被当作值使用。自然,这就意味着它们有自己的类型。例如:
fn city_population_descending(city: &City) -> i64 {
-city.population
}
该函数会接受一个参数(&City)并返回 i64。所以它的类型是 fn(&City) -> i64。你可以像对其他值一样对函数执行各种操作。你可以将函数存储在变量中,也可以使用所有常用的 Rust 语法来计算函数值:

let my_key_fn: fn(&City) -> i64 =
if user.prefs.by_population {
city_population_descending
} else {
city_monster_attack_risk_descending
};

cities.sort_by_key(my_key_fn);
结构体也可以有函数类型的字段。像 Vec 这样的泛型类型可以存储大量的函数,只要它们共享同一个 fn 类型即可。而且函数值占用的空间很小,因为 fn 值就是函数机器码的内存地址,就像 C++ 中的函数指针一样。一个函数还可以将另一个函数作为参数:

/// 给定一份城市列表和一个测试函数,返回有多少个城市通过了测试
fn count_selected_cities(cities: &Vec,
test_fn: fn(&City) -> bool) -> usize
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}

/// 测试函数的示例。注意,此函数的类型是fn(&City) -> bool
/// 与count_selected_citiestest_fn参数相同
fn has_monster_attacks(city: &City) -> bool {
city.monster_attack_risk > 0.0
}

// 有多少个城市存在被怪兽袭击的风险?
let n = count_selected_cities(&my_cities, has_monster_attacks);

如果你熟悉 C/C++ 中的函数指针,就会发现 Rust 的函数值简直跟它一模一样。知道了这些之后,说闭包与函数不是同一种类型可能会让人大吃一惊:

let limit = preferences.acceptable_monster_risk();
let n = count_selected_cities(
&my_cities,
|city| city.monster_attack_risk > limit); // 错误:类型不匹配

第二个参数会导致类型错误。为了支持闭包,必须更改这个函数的类型签名。要改成下面这样:

fn count_selected_cities(cities: &Vec, test_fn: F) -> usize
where F: Fn(&City) -> bool
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}

这里只更改了 count_selected_cities 的类型签名,而没有更改函数体。新版本是泛型函数。只要 F 实现了特定的特型 Fn(&City) -> bool,该函数就能接受任意 F 型的 test_fn。以单个 &City 为参数并返回 bool 值的所有函数和大多数闭包会自动实现这个特型:

fn(&City) -> bool // fn类型(只接受函数)
Fn(&City) -> bool // Fn特型(既接受函数也接受闭包)

这种特殊的语法内置于语言中。-> 和返回类型是可选的,如果省略,则返回类型为 ()。count_selected_cities 的新版本会接受函数或闭包:

count_selected_cities(
&my_cities,
has_monster_attacks); // 正确

count_selected_cities(
&my_cities,
|city| city.monster_attack_risk > limit); // 同样正确
为什么第一次尝试没有成功?好吧,闭包确实是可调用的,但它不是 fn。闭包 |city| city.monster_attack_risk > limit 有它自己的类型,但不是 fn 类型。事实上,你编写的每个闭包都有自己的类型,因为闭包可以包含数据:从封闭作用域中借用或“窃取”的值。这既可以是任意数量的变量,也可以是任意类型的组合。所以每个闭包都有一个由编译器创建的特殊类型,大到足以容纳这些数据。任何两个闭包的类型都不相同。但是每个闭包都会实现 Fn 特型,我们示例中的闭包就实现了 Fn(&City) -> i64。因为每个闭包都有自己的类型,所以使用闭包的代码通常都应该是泛型的,比如 count_selected_cities。每次都明确写出泛型类型确实有点儿笨拙,如果想了解这种设计的优点,请继续往下阅读。

闭包性能

Rust 中闭包的设计目标是要快:比函数指针还要快,快到甚至可以在对性能敏感的热点代码中使用它们。如果你熟悉 C++ 的 lambda 表达式,就会发现 Rust 闭包也一样快速而紧凑,但更安全。在大多数语言中,闭包会在堆中分配内存、进行动态派发以及进行垃圾回收。因此,创建、调用和收集每一个闭包都会花费一点点额外的 CPU 时间。更糟的是,闭包往往难以内联,而内联是编译器用来消除函数调用开销并实施大量其他优化的关键技术。总而言之,闭包在这些语言中确实慢到值得手动将它们从节奏紧凑的内层循环中去掉。Rust 闭包则没有这些性能缺陷。它们没有垃圾回收。与 Rust 中的其他所有类型一样,除非你将闭包放在 Box、Vec 或其他容器中,否则它们不会被分配到堆上。由于每个闭包都有不同的类型,因此 Rust 编译器只要知道你正在调用的闭包的类型,就可以内联该闭包的代码。这使得在节奏紧凑的循环中使用闭包成为可能,并且各种 Rust 程序经常会满怀热情地刻意这么做,你会在第 15 章中亲自体会到这一点。图 14-1 展示了 Rust 闭包在内存中的布局方式。在图的顶部,我们展示了闭包要引用的两个局部变量:字符串 food 和简单的枚举 weather,枚举的数值恰好是 27。

在这里插入图片描述

闭包 (a) 使用了上述两个变量。显然,我们正在寻找既有炸玉米饼(taco)又有龙卷风(tornado)的城市。在内存中,这个闭包看起来像一个小型结构体,其中包含对其所用变量的引用。请注意,这个闭包并不包含指向其代码的指针。这种指针毫无必要:只要 Rust 知道闭包的类型,就知道在调用此闭包时该运行哪些代码。闭包 (b) 与闭包 (a) 完全相同,只不过它是一个 move 闭包,因此会包含值而非引用。闭包 © 不会使用其环境中的任何变量。该结构体是空的,所以这个闭包根本不会占用任何内存。如图 14-1 所示,这些闭包不会占用多少空间。但在实践中,即使那区区几字节可能也不是必要的。通常,编译器会内联所有对闭包的调用,然后连图中所示的小结构体也优化掉。14.5 节会展示如何使用特型对象在堆中分配闭包并动态调用它们。虽然这种方法有点儿慢,但仍然和特型对象的其他方法一样快。

闭包与安全

“杀死”闭包

我们已经见过借用值的闭包和“窃取”值的闭包,如果沿着这种思路一直走下去,那么早晚会“出事”。当然,“杀死”并不是正确的术语。在 Rust 中,我们会说丢弃值。最直观的方法是调用 drop():

let my_str = “hello”.to_string();
let f = || drop(my_str);

当调用 f 时,my_str 会被丢弃。那么,如果调用它两次会发生什么呢?

f();
f();

我们来深入思考一下。当第一次调用 f 时,它丢弃了 my_str,这意味着存储该字符串的内存已经被释放,交还给了系统。当第二次调用 f 时,发生了同样的事情。这是 C++ 编程中会触发未定义行为的经典错误:双重释放。在 Rust 中丢弃同一个 String 两次当然也会出事。幸运的是,Rust 可没那么容易被愚弄:

f(); // 正确
f(); // 错误:使用了已移动的值

Rust 知道这个闭包不能调用两次。一个只能调用一次的闭包看起来很不寻常,但这种现象的根源在于本书一直在讲的所有权和生命周期。值会被消耗掉(移动)的想法是 Rust 的核心概念之一,它对闭包与其他语法一视同仁。

FnOnce

我们再试一次欺骗 Rust,让它把同一个 String 丢弃两次。这次将使用下面这个泛型函数:

fn call_twice(closure: F) where F: Fn() {
closure();
closure();
}

可以将这个泛型函数传给任何实现了特型 Fn() 的闭包,即不带参数且会返回 () 的闭包。(与函数一样,返回类型如果是 () 则可以省略,Fn() 是 Fn() -> () 的简写形式。)现在,如果将不安全的闭包传给这个泛型函数会发生什么呢?
let my_str = “hello”.to_string();
let f = || drop(my_str);
call_twice(f);

同样,此闭包将在调用后丢弃 my_str。调用它两次将导致双重释放。但 Rust 仍然没有被愚弄:

error: expected a closure that implements the Fn trait, but this closure only implements FnOnce

这条错误消息为我们揭示了关于 Rust 如何处理“清理型闭包”的更多信息。Rust 本可以在语言中完全禁止这种闭包,但清理闭包有时候是很有用的。因此,Rust 只是限制了它们的使用场景。像 f 这种会丢弃值的闭包不允许实现 Fn。从字面上看,它们也确实不是 Fn。它们实现了一个不那么强大的特型 FnOnce,即只能调用一次的闭包特型。第一次调用 FnOnce 闭包时,闭包本身也会被消耗掉。这是因为 Fn 和 FnOnce 这两个特型是这样定义的:

// 无参数的Fn特型和FnOnce特型的伪代码
trait Fn() -> R {
fn call(&self) -> R;
}

trait FnOnce() -> R {
fn call_once(self) -> R;
}

正如算术表达式 a + b 是方法调用 Add::add(a, b) 的简写形式一样,Rust 也会将 closure() 视为前面示例中的两个特型方法之一的简写形式。对于 Fn 闭包,closure() 会扩展为 closure.call()。此方法会通过引用获取 self,因此闭包不会被移动。但是如果闭包只能安全地调用一次,那么 closure() 就会扩展为 closure.call_once()。该方法会按值获取 self,因此这个闭包就会被消耗掉。当然,这里是故意使用 drop() 挑起的麻烦。而在实践中,你通常都会在无意中遇到这种情况。虽然不会经常发生,但偶尔你还是会写出一些无意中消耗掉一个值的闭包代码:

let dict = produce_glossary();
let debug_dump_dict = || {
for (key, value) in dict { // 糟糕!
println!(“{:?} - {:?}”, key, value);
}
};

然后,当你多次调用 debug_dump_dict() 时,就会收到如下错误消息:

error: use of moved value: debug_dump_dict
要调试上述错误,就必须弄清楚此闭包为什么是 FnOnce。这里使用了哪个值?编译器友好地指出它是 dict,在这种情况下该值是我们唯一引用的值。啊,果然有一个 bug:它通过直接迭代消耗掉了 dict。我们应该遍历 &dict,而不是普通的 dict,以便通过引用访问值:
let debug_dump_dict = || {
for (key, value) in &dict { // 不要消耗掉dict
println!(“{:?} - {:?}”, key, value);
}
};

这样就修复了错误,现在这个函数是 Fn 并且可以调用任意次数了。

FnMut
还有一种包含可变数据或可变引用的闭包。Rust 认为不可变值可以安全地跨线程共享,但是包含可变数据的不可变闭包不能安全共享——从多个线程调用这样的闭包可能会导致各种竞态条件,因为多个线程会试图同时读取和写入同一份数据。Rust 还有另一类名为 FnMut 的闭包,也就是可写入的闭包。FnMut 闭包会通过可变引用来调用,其定义如下所示:

// Fn特型、FnMut特型和FnOnce特型的伪代码
trait Fn() -> R {
fn call(&self) -> R;
}

trait FnMut() -> R {
fn call_mut(&mut self) -> R;
}

trait FnOnce() -> R {
fn call_once(self) -> R;
}

任何需要对值进行可变访问但不会丢弃任何值的闭包都是 FnMut 闭包。例如:

let mut i = 0;
let incr = || {
i += 1; // incr借入了对i的一个可变引用
println!(“Ding! i is now: {}”, i);
};
call_twice(incr);

按照 call_twice 的调用方式,它会要求传入一个 Fn。由于 incr 是 FnMut 而非 Fn,因此上述代码无法通过编译。不过,有一种简单的解决方法。为了理解此修复,我们先回过头来总结一下 Rust 闭包的 3 种类别。Fn 是可以不受限制地调用任意多次的闭包和函数系列。此最高类别还包括所有 fn 函数。FnMut 是本身会被声明为 mut,并且可以多次调用的闭包系列。FnOnce 是如果其调用者拥有此闭包,它就只能调用一次的闭包系列。每个 Fn 都能满足 FnMut 的要求,每个 FnMut 都能满足 FnOnce 的要求。如图 14-2 所示,它们不是 3 个彼此独立的类别。

在这里插入图片描述

应该说,Fn() 是 FnMut() 的子特型,而 FnMut() 是 FnOnce() 的子特型。这使得 Fn 成了最严格且最强大的类别。FnMut 和 FnOnce 是更宽泛的类别,其中包括某些具有使用限制的闭包。现在我们已经厘清了思路,很显然为了接受尽可能宽泛的闭包,call_twice 函数应该接受所有 FnMut 闭包,如下所示:

fn call_twice(mut closure: F) where F: FnMut() {
closure();
closure();
}

第 1 行的限界原来是 F: Fn(),现在是 F: FnMut()。通过此更改,我们仍然能接受所有 Fn 闭包,并且还可以在会修改数据的闭包上使用 call_twice。

let mut i = 0;
call_twice(|| i += 1); // 正确!
assert_eq!(i, 2);

对闭包的 Copy 与 Clone
就像能自动找出哪些闭包只能调用一次一样,Rust 也能找出哪些闭包可以实现 Copy 和 Clone,哪些则不可以实现。正如之前所解释的,闭包是表示包含它们捕获的变量的值(对于 move 闭包)或对值的引用(对于非 move 闭包)的结构体。闭包的 Copy 规则和 Clone 规则与常规结构体的规则是一样的。一个不修改变量的非 move 闭包只持有共享引用,这些引用既能 Clone 也能 Copy,所以闭包也能 Clone 和 Copy:

let y = 10;
let add_y = |x| x + y;
let copy_of_add_y = add_y; // 此闭包能Copy,所以……
assert_eq!(add_y(copy_of_add_y(22)), 42); // ……可以调用它两次

另外,一个会修改值的非 move 闭包在其内部表示中也可以有可变引用。可变引用既不能 Clone,也不能 Copy,使用它们的闭包同样如此:

let mut x = 0;
let mut add_to_x = |n| { x += n; x };

let copy_of_add_to_x = add_to_x; // 这会进行移动而非复制
assert_eq!(add_to_x(copy_of_add_to_x(1)), 2); // 错误:使用了已移动出去的值

对于 move 闭包,规则更简单。如果 move 闭包捕获的所有内容都能 Copy,那它就能 Copy。如果 move 闭包捕获的所有内容都能 Clone,那它就能 Clone。例如:

let mut greeting = String::from(“Hello, “);
let greet = move |name| {
greeting.push_str(name);
println!(”{}”, greeting);
};
greet.clone()(“Alfred”);
greet.clone()(“Bruce”);

这里的 .clone()(…) 语法有点儿奇怪,其实它只是表示克隆此闭包并调用其克隆体。这个程序会输出如下内容:

Hello, Alfred
Hello, Bruce

当在 greet 中使用 greeting 时,greeting 被移动到了内部表示 greet 的结构体中,因为它是一个 move 闭包。所以,当我们克隆 greet 时,它里面的所有东西同时被克隆了。greeting 有两个副本,它们会在调用 greet 的克隆时分别被修改。这种行为本身并不是很有用,但是在你需要将同一个闭包传给多个函数的场景中,它会非常有帮助。

回调

部分路由器程序:

App::new()
.route(“/”, web::get().to(get_index))
.route(“/gcd”, web::post().to(post_gcd))

这个路由器的目的是将从互联网传入的请求路由到处理特定类型请求的那部分 Rust 代码中。在本示例中,get_index 和 post_gcd 是我们在程序其他地方使用 fn 关键字声明的函数名称。其实也可以在这里传入闭包,就像这样:

App::new()
.route(“/”, web::get().to(|| {
HttpResponse::Ok()
.content_type(“text/html”)
.body(“GCD Calculator…”)
}))
.route(“/gcd”, web::post().to(|form: web::Form| {
HttpResponse::Ok()
.content_type(“text/html”)
.body(format!(“The GCD of {} and {} is {}.”,
form.n, form.m, gcd(form.n, form.m)))
}))

这是因为 actix-web 设计成了可以接受任何线程安全的 Fn 作为参数的形式。那么,如何在自己的程序中做到这一点呢?可以试着从头开始编写自己的简易路由器,而不使用来自 actix-web 的任何代码。可以首先声明一些类型来表示 HTTP 请求和响应:

struct Request {
method: String,
url: String,
headers: HashMap<String, String>,
body: Vec
}

struct Response {
code: u32,
headers: HashMap<String, String>,
body: Vec
}

现在路由器所做的只是存储一个将 URL 映射到回调的表,以便按需调用正确的回调。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值