Pop
push と同じように、pop はリストを変更したがります。push と違って、実際には
何かを返したいです。しかし pop は厄介なコーナーケースにも対処しなければ
なりません。リストが空だったらどうするのでしょうか? このケースを表現するために、
頼れる Option 型を使います。
pub fn pop(&mut self) -> Option<i32> {
// TODO
}
Option<T> は、存在するかもしれない値を表す enum です。これは
Some(T) または None のどちらかになります。Link で行ったように、
このために独自の enum を作ることもできますが、ユーザーには私たちの戻り値の型が
一体何なのかを理解してもらいたいですし、Option はあまりにも広く使われているので
誰もが 知っています。実際、これは非常に基本的なものなので、すべてのファイルで
暗黙的にスコープにインポートされますし、そのバリアントである Some と None も
同様です(そのため Option::None と書く必要はありません)。
Option<T> の山括弧の部分は、Option が実際には T に対して ジェネリック である
ことを示しています。つまり、任意の 型について Option を作れるということです!
さて、この Link というものがありますが、これが Empty なのか More を持っているのかは
どうやって判断するのでしょうか? match によるパターンマッチングです!
pub fn pop(&mut self) -> Option<i32> {
match self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
}
> cargo build
error[E0308]: mismatched types
--> src/first.rs:27:30
|
27 | pub fn pop(&mut self) -> Option<i32> {
| --- ^^^^^^^^^^^ expected enum `std::option::Option`, found ()
| |
| this function's body doesn't return
|
= note: expected type `std::option::Option<i32>`
found type `()`
おっと、pop は値を返さなければなりませんが、まだそれをしていません。None を
返すことも できます が、この場合は、まだこの関数の実装が終わっていないことを
示すために unimplemented!() を返すほうがおそらくよいでしょう。
unimplemented!() はマクロ(! はマクロであることを示します)で、そこに到達すると
プログラムをパニックさせます(制御された形でクラッシュさせるようなものです)。
pub fn pop(&mut self) -> Option<i32> {
match self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
unimplemented!()
}
無条件のパニックは、発散関数 の一例です。発散関数は呼び出し元に
決して戻らないため、任意の型の値が期待される場所で使うことができます。ここでは、
unimplemented!() が Option<T> 型の値の代わりに使われています。
また、プログラム内で return を書く必要がないことにも注意してください。関数内の
最後の式(基本的には行)が暗黙的にその戻り値になります。これにより、本当に単純な
ことを少し簡潔に表現できます。他の C 系言語と同じように、return を使って明示的に
早期リターンすることも常にできます。
> cargo build
error[E0507]: cannot move out of borrowed content
--> src/first.rs:28:15
|
28 | match self.head {
| ^^^^^^^^^
| |
| cannot move out of borrowed content
| help: consider borrowing here: `&self.head`
...
32 | Link::More(node) => {
| ---- data moved here
|
note: move occurs because `node` has type `std::boxed::Box<first::Node>`, which does not implement the `Copy` trait
--> src/first.rs:32:24
|
32 | Link::More(node) => {
| ^^^^
勘弁してくれ Rust、もう放っておいて! いつものように、Rust は私たちにめちゃくちゃ
怒っています。ありがたいことに、今回は詳細も全部教えてくれています! デフォルトでは、
パターンマッチはその中身を新しい分岐へムーブしようとしますが、ここでは self を
値として所有していないので、それはできません。
help: consider borrowing here: `&self.head`
Rust は、これを修正するために match に参照を追加すべきだと言っています。🤷♀️ 試してみましょう。
pub fn pop(&mut self) -> Option<i32> {
match &self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
unimplemented!()
}
> cargo build
warning: unused variable: `node`
--> src/first.rs:32:24
|
32 | Link::More(node) => {
| ^^^^ help: consider prefixing with an underscore: `_node`
|
= note: #[warn(unused_variables)] on by default
warning: field is never used: `elem`
--> src/first.rs:13:5
|
13 | elem: i32,
| ^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `next`
--> src/first.rs:14:5
|
14 | next: Link,
| ^^^^^^^^^^
やった、またコンパイルできました! では、そのロジックを考えてみましょう。Option を
作りたいので、そのための変数を作りましょう。Empty の場合は None を返す必要があります。
More の場合は Some(i32) を返し、リストの head を変更する必要があります。では、
基本的にそれをやってみましょうか?
pub fn pop(&mut self) -> Option<i32> {
let result;
match &self.head {
Link::Empty => {
result = None;
}
Link::More(node) => {
result = Some(node.elem);
self.head = node.next;
}
};
result
}
> cargo build
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0507]: cannot move out of borrowed content
--> src/first.rs:35:29
|
35 | self.head = node.next;
| ^^^^^^^^^ cannot move out of borrowed content
head
desk
共有参照しか持っていないのに、node からムーブしようとしています。
一歩戻って、何をしようとしているのか考えたほうがよさそうです。やりたいことは 次のとおりです。
- リストが空かどうかを確認する。
- 空なら、単に None を返す
- 空で ない なら
- リストの head を取り除く
- その
elemを取り出す - リストの head をその
nextに置き換える Some(elem)を返す
重要な洞察は、私たちはものを 取り除きたい ということです。つまり、リストの head を
値として 取得したいということです。&self.head から得られる共有参照を通じて、
それを行うことは絶対にできません。また、私たちは self への可変参照「しか」
持っていないので、何かをムーブする唯一の方法は、それを 置き換える ことです。
また Empty ダンスをすることになりそうです!
試してみましょう。
pub fn pop(&mut self) -> Option<i32> {
let result;
match mem::replace(&mut self.head, Link::Empty) {
Link::Empty => {
result = None;
}
Link::More(node) => {
result = Some(node.elem);
self.head = node.next;
}
};
result
}
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
O M G
どんな 警告もなくコンパイルできました!!!!!
実は、ここで個人的な lint を適用します。返すためにこの result という値を
作りましたが、実際にはまったく必要ありませんでした! 関数が最後の式として
評価されるのと同じように、すべてのブロックもその最後の式として評価されます。
通常はセミコロンでこの振る舞いを抑制します。その場合、ブロックは代わりに空の
タプル () として評価されます。これは実際、push のように戻り値を宣言しない
関数が返す値です。
したがって、代わりに pop は次のように書けます。
pub fn pop(&mut self) -> Option<i32> {
match mem::replace(&mut self.head, Link::Empty) {
Link::Empty => None,
Link::More(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
こちらのほうが少し簡潔で慣用的です。Link::Empty 分岐では波括弧が完全になくなっている ことに注意してください。評価する式が 1 つしかないからです。単純なケースのための、 ちょっと便利な省略記法です。
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
いいですね、まだ動きます!