Pop メソッド
push
と同様,pop
はリストを変更するメソッドです.
push
と異なるところは,実際に何かしらの値を返すことですね.
pop
はリストが空だったらどうするかという厄介なコーナーケースも扱う必要があります.
このケースに対処するために,Option
型という頼もしい道具を使用します:
pub fn pop(&mut self) -> Option<i32> {
// TODO
}
Option<T>
は存在するかどうかわからない値を扱う列挙型です.
Some(T)
か None
かのいずれかの値を取ります.
Link
のときにやったように独自の enum
を作ってもいいのですが,ユーザに戻り値が何なのか理解してほしいので,
どこにでもあり 誰もが 知っている Option
型を使うのが得策です.
実際,Option
は非常に基本的なものなので,すべてのファイルで暗黙のうちにスコープに取り込まれます.
そのヴァリアントである Some
や None
も同様です.
(ですから,Option::None
などと書く必要はないのです)
Option<T>
の <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!()
}
無条件のパニックは,発散する関数 (diverging function) の例です.
発散する関数は呼び出し元に戻ることがないので,本来期待される値の型がなんであったとしても,
使用することができます.
ここでは,Option<T>
型の代わりに unimplemented!()
を使用しています.
また,プログラム中に 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`
ヘルプ: 借用することを検討してみてください: `&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,
| ^^^^^^^^^^
万歳,コンパイルが通ったぞ!
それでは,TODO で残しておいたロジック部分を考えていきましょう.
Option
を作りたいので,そのための変数を作ります.
Empty
の場合には None
を返さないといけませんね.
More
の場合には Some(i32)
を返して,リストの先頭を変更する必要があります.
おおまかにはこんな感じでどうでしょうか?
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/ABeingessner/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
ゴン!(頭を机に叩きつけた音)
共有参照しているものを,node
からムーブしようとしてしまったようです.
立ち止まって,これからやろうとしていることが何なのか考え直すべきでしょう. 私たちがやりたいのは次のようなことです:
- リストが空かどうかチェックする
- 空であれば,
None
を返せばいいです - 空でなかったときは以下を実行します:
- リストの先頭を削除する
elem
を削除する- リストの先頭を
next
で置き換える Some(elem)
を返す
重要なことは,私たちが 削除 をしようとしているという点です.
これはつまるところリストの先頭を(所有権のある)値として取得する必要があることを意味します.
共有参照である &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
ありえない….
警告を ひとつも 出さずにコンパイルできました!!!!!
個人的なこだわりですが,ここでコード整形をしましょう: result
という変数を使いましたが,実はこれは不要です.
関数が最後の式で評価されるのと同じように,match
の各ブロックも最後の式で評価されます.
通常はセミコロンを付けて文にすることでこの挙動を抑制し,ブロックの評価を空のタプル ()
にしますね.
この ()
は 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
ちゃんとコンパイルも通りました.いいね!