Pop

push一样,pop想要改变列表;除此之外,我们还想返回结果。然而pop还得处理一个特殊的边界情况:如果列表是空的呢?为了表示这个情况,我们使用可靠的Option类型:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. //TODO
  3. }

Option<T>是一个表示一个值可能存在也可能不存在的enum。它要么是Some(T),要么是None。我们也可以像Link一样创建一个自己的enum,但是我们想让用户了解我们的返回类型到底是什么,而Option是如此的无处不在,每个人都知道它。实际上,因为它是如此的基本,它被隐式的导入到了每一个源文件的作用域中,也包括它的两个变体:SomeNone(这样我们就不用写Option::None)。

Option<T>尖括号里的部分指出Option实际上是一个泛型,它的泛型参数是T。这意味着你可以创建一个任何类型的Option!

所以,我们有这个Link了,我们怎么检查它是Empty还是More呢?使用match进行模式匹配:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. match self.head {
  3. Link::Empty => {
  4. // TODO
  5. }
  6. Link::More(node) => {
  7. // TODO
  8. }
  9. };
  10. }
  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/lists)
  3. src/first.rs:27:2: 36:3 error: not all control paths return a value [E0269]
  4. src/first.rs:27 pub fn pop(&mut self) -> Option<i32> {
  5. src/first.rs:28 match self.head {
  6. src/first.rs:29 Link::Empty => {
  7. src/first.rs:30 // TODO
  8. src/first.rs:31 }
  9. src/first.rs:32 Link::More(node) => {
  10. ...
  11. error: aborting due to previous error
  12. Could not compile `lists`.

啊,pop必须返回一个值,我们还没做这件事。我们可以直接返回None,但是在这情况下,返回unimplemented!来指出我们没有完成该函数的实现会更好。unimplemented!是一个宏(!代表一个宏),它会在被调用的时候让整个程序panic(基本上也就是以可控的方式崩溃)。

  1. pub fn pop(&mut self) -> Option<i32> {
  2. match self.head {
  3. Link::Empty => {
  4. // TODO
  5. }
  6. Link::More(node) => {
  7. // TODO
  8. }
  9. };
  10. unimplemented!()
  11. }

无条件panic是一个发散函数(diverging function)的例子。发散函数永远不会返回到调用者,所以无论一个地方期待何种类型的返回值,它的返回值都能拿来用。在这里,unimplemented!被使用在期待一个Option<T>的地方。

注意到我们不需要在程序里写return。函数中的最后一个表达式也就隐式的成为它的返回值。这让我们可以更精炼的表达简单的逻辑。你也可以像C系语言一样,显式的return返回。

  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/lists)
  3. src/first.rs:28:9: 28:13 error: cannot move out of borrowed content
  4. src/first.rs:28 match self.head {
  5. ^~~~
  6. src/first.rs:32:15: 32:19 note: attempting to move value to here
  7. src/first.rs:32 Link::More(node) => {
  8. ^~~~
  9. src/first.rs:32:15: 32:19 help: to prevent the move, use `ref node` or `ref mut node` to capture value by reference
  10. error: aborting due to previous error
  11. Could not compile `lists`.

噢,Rust,别纠缠不休了!和往常一样,Rust对我们非常生气。值得感谢的是,这次它还给出了深入的信息!

  1. src/first.rs:17:9: 17:13 error: cannot move out of borrowed content
  2. src/first.rs:17 match self.head {
  3. ^~~~
  4. src/first.rs:21:15: 21:19 note: attempting to move value to here
  5. src/first.rs:21 Link::More(node) => {
  6. ^~~~

模式匹配默认会移动匹配的值,所以这就是它如此生气的原因。

  1. help: to prevent the move, use `ref node` or `ref mut node` to capture value by reference
  2. 帮助:要避免移动,使用`ref node``ref mut node`来引用捕获该值

为了避免移动,我们使用ref关键词来指明我们想要把node进行引用绑定。来试试吧:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. match self.head {
  3. Link::Empty => {
  4. // TODO
  5. }
  6. Link::More(ref node) => {
  7. // TODO
  8. }
  9. };
  10. unimplemented!()
  11. }
  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/lists)
  3. src/first.rs:13:2: 13:9 warning: struct field is never used: `elem`, #[warn(dead_code)] on by default
  4. src/first.rs:13 elem: i32,
  5. ^~~~~~~
  6. src/first.rs:14:2: 14:15 warning: struct field is never used: `next`, #[warn(dead_code)] on by default
  7. src/first.rs:14 next: Link<T>,
  8. ^~~~~~~~~~~~~
  9. src/first.rs:32:15: 32:23 warning: unused variable: `node`, #[warn(unused_variables)] on by default
  10. src/first.rs:32 Link::More(ref node) => {
  11. ^~~~~~~~

好耶,又编译了!现在让我们搞清楚实现逻辑。我们要创建一个Option,所以要为这个预留一个变量。在Empty情况下要返回None,在More情况下需要返回Some(i32),并且改变列表的head。来吧:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. let result;
  3. match self.head {
  4. Link::Empty => {
  5. result = None;
  6. }
  7. Link::More(ref node) => {
  8. result = Some(node.elem);
  9. self.head = node.next;
  10. }
  11. };
  12. result
  13. }
  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
  3. src/first.rs:39:29: 39:33 error: cannot move out of borrowed content
  4. src/first.rs:39 self.head = node.next;
  5. ^~~~
  6. src/first.rs:39:17: 39:38 error: cannot assign to `self.head` because it is borrowed
  7. src/first.rs:39 self.head = node.next;
  8. ^~~~~~~~~~~~~~~~~~~~~
  9. src/first.rs:37:24: 37:32 note: borrow of `self.head` occurs here
  10. src/first.rs:37 Link::More(ref node) => {
  11. ^~~~~~~~
  12. error: aborting due to 2 previous errors
  13. Could not compile `lists`.

我们现在有了两个不同的的错误。。首先,我们在仅仅拥有一个共享引用的情况下就尝试把值移动出node。其次,在我们已经租借了node的引用的时候,还在尝试改变self.head的值!

真是一堆纠缠不清的乱东西。

我们应该后退一步,思考我们要做什么。我们想要:

  • 检查列表是否为空。
  • 如果是空的,返回None
  • 如果是非空
    • 移除list头部
    • 移除该头部的elem
    • 将列表的head替换为next
    • 返回Some(elem)

重要的一点事我们想要删除东西,这意味着我们需要按值获取list的head。我们肯定不能通过由ref node获取的共享引用来做这件事。我们也“只”拥有一个可变引用,所以能移动东西的唯一方法就是替换它。看来我们又在做Empty替换那一套了!来试试吧:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. let result;
  3. match mem::replace(&mut self.head, Link::Empty) {
  4. Link::Empty => {
  5. result = None;
  6. }
  7. Link::More(node) => {
  8. result = Some(node.elem);
  9. self.head = node.next;
  10. }
  11. };
  12. result
  13. }
  1. cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)

我 的 天 哪

它编译了,一个警告都没有!!!!!

这里我要给出我的优化提示了:我们现在返回的是result变量的值,但实际上根本不用这么做!就像一个函数的结果是它的最后一个表达式,每个代码块的结果也是它的最后一个表达式。通常我们使用分号来阻止这一行为,这会让代码块的值变成空元组(tuple)()。这实际上也是不声明返回值的函数——例如push——返回的。

因此,我们可以将pop修改为:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. match mem::replace(&mut self.head, Link::Empty) {
  3. Link::Empty => None,
  4. Link::More(node) => {
  5. self.head = node.next;
  6. Some(node.elem)
  7. }
  8. }
  9. }

这更简洁,也更符合语言惯例。注意到Link::Empty分支只需要求值一个表达式,所以我们把大括号也去掉了。这是对于简单情况的简便处理。

  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
  3. src/first.rs:36:22: 36:31 error: use of moved value: `node` [E0382]
  4. src/first.rs:36 Some(node.elem)
  5. ^~~~~~~~~
  6. src/first.rs:35:29: 35:38 note: `node` moved here (through moving `node.next`) because it has type `first::Link`, which is non-copyable
  7. src/first.rs:35 self.head = node.next;
  8. ^~~~~~~~~
  9. error: aborting due to previous error

啥?别这样啊。

为什么我们的代码不工作了?!

实际上,我们之前的代码只是侥幸通过了编译,借助Copy的魔法。当我们介绍所有权的时候说过,当你移动值的时候,就无法再使用它。对于某些类型,这是完全合理的。我们的好朋友Box为我们管理堆中的内存分配,而我们显然不想让两段代码认为它们应该释放相同的一块内存。

但是对某些类型这简直糟透了。整数可没有所有权语义:它们只是毫无意义的数字!这也正是为什么整数被标记为Copy。Copy类型可以通过按位复制进行完整的拷贝。因此,它们拥有一个超能力:当被移动的时候,老的值仍然是可用的。作为结果,你可以将一个Copy类型从引用移出而不需替换!

所有rust中的基本数字类型(i32, u64, bool, f32, char, etc…)都是Copy。同时,共享引用也是Copy,这很有用!只要一个自定类型的所有字段都是Copy,你也可以将该类型声明为Copy。

总之,回到代码:出了什么错?在第一次迭代过程中,我们对result进行赋值时在进行拷贝,因此node没被改变,可以被继续用于下一个操作。现在我们在移动next(它不是Copy),而这在我们能碰到elem之前消耗掉了整个Box里的值。

现在,我们可以重新调整代码来只拿到elem,但我们只是使用i32作为某种数据的占位符。晚些时候我们会处理非Copy类型的数据,所以最好现在就研究研究怎么做。

正确的答案是将整个节点从Box中取出来,这样就可以安全的将它拆开了。我们通过显式解引用操作来做这件事:

  1. pub fn pop(&mut self) -> Option<i32> {
  2. match mem::replace(&mut self.head, Link::Empty) {
  3. Link::Empty => None,
  4. Link::More(boxed_node) => {
  5. let node = *boxed_node;
  6. self.head = node.next;
  7. Some(node.elem)
  8. }
  9. }
  10. }

在这之后,Rust就可以足够好的理解一个栈上的值,来让你一步步分解它了。

  1. > cargo build
  2. Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)

不错。

Box在Rust里真的很特殊,因为它是固定于语言里的一部分,编译器可以让你对它做一些其它任何类型都不能做的事。我们实际上一直在做这件事:DerefMove。当你拥有一个指针类型时,可以通过*.来获得它的内容。通常你可以获得一个Deref或者一个DerefMut,分别对应共享和可变引用。

但是因为Box完全拥有它的内容,你可以通过解引用将内容移出。这是完完全全的魔法,因为其他任何类型都无法实现这个操作。编译器还知道如何在Box上实现很多很多的酷炫技巧,只因为它就是Box,但是它们都被阻挡在了1.0版本的实现目标之外,等待进一步的设计。理想的,Box会在未来完全可自定义化。