Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pop

push と同じように、pop はリストを変更したがります。push と違って、実際には 何かを返したいです。しかし pop は厄介なコーナーケースにも対処しなければ なりません。リストが空だったらどうするのでしょうか? このケースを表現するために、 頼れる Option 型を使います。

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

Option<T> は、存在するかもしれない値を表す enum です。これは Some(T) または None のどちらかになります。Link で行ったように、 このために独自の enum を作ることもできますが、ユーザーには私たちの戻り値の型が 一体何なのかを理解してもらいたいですし、Option はあまりにも広く使われているので 誰もが 知っています。実際、これは非常に基本的なものなので、すべてのファイルで 暗黙的にスコープにインポートされますし、そのバリアントである SomeNone も 同様です(そのため 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

いいですね、まだ動きます!