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
ちゃんとコンパイルも通りました.いいね!