コンテナの値を取って→入れて→エラー (lifetimeとかの話)

  • 20
    いいね
  • 1
    コメント

はじめに

Rustのコンテナを使うときは、所有権を強く意識する必要があります。 HashMapはその代表的な例で、HashMapに要素を挿入するときは値渡しを要求され、HashMapの要素の値を見るときには参照渡しでしか返してくれません。 つまりHashMapは挿入された要素の所有権を奪うぞということで、まぁそういうものだと言われればそうだよねという話なのですが、ここでよくハマるポイントがあります。

フィボナッチ数列のHashMapを作ってみる例
use std::collections::HashMap;

fn main() {
    let mut fib : HashMap<i32, i32> = HashMap::new();
    fib.insert(1, 1);
    fib.insert(2, 1);
    fib.insert(3, 2);
    fib.insert(4, 3);
    fib.insert(5, 5);

    let fib4 : &i32 = fib.get(&4).unwrap();
    let fib5 : &i32 = fib.get(&5).unwrap();
    // コンパイルエラー!
    // cannot borrow `fib` as mutable because it is also borrowed as immutable
    fib.insert(6, fib4 + fib5);
}

この例ではフィボナッチ数列のHashMapを作っています。 フィボナッチ数列の6番目は4番目+5番目ですから、当然上のようにして作成するのですが、これはコンパイルが通りません。
コンパイルエラーは分かりやすく、『fibはすでに参照が使われているから、&mut selfな呼び出しのinsertメソッドは使えないよ』みたいなことを言ってくれます。 ある変数へのmut参照と非mut参照は同時に存在できないということは、Rustの参照を最初に学ぶときに口を酸っぱくして言われることなので、このコンパイルエラーにも納得してしまいそうになりますが、ちょっと待ってください上のコードのどこに、fibへの『生きてる』参照があるんだ?

確かにこの行で、fibへの参照を利用してはいます。

    let fib4 : &i32 = fib.get(&4).unwrap();

HashMap::getメソッドの第1引数は&selfですから、getメソッドに渡っているのはfibへの参照です。 しかし、ここでは参照を一時的に作って渡しているだけで、よく例題に出されるようなこんな文とは明らかに異なります。

よく所有権の例題で出されるやつ
fn owner() {
    let mut x = 100;
    let ref_x = &x;

    // 非mut参照とmut参照は同時に存在できないのでエラーになる
    let mut_ref_x = &mut x;
}

この例題では、明らかにxへの参照を変数ref_xに保持しています。 なので、その下でxへのmut参照を取ろうとしてエラーになるのはよく分かります。 しかし、フィボナッチ数列の例では、fibへの参照は一時的に使われているけれども、その結果変数fib4に代入されているのは&i32型です。 i32への参照をfib4に入れてるだけなのに、なんでfibへの参照を作っているようにコンパイラは解釈してしまうんだ? という疑問が当然湧き上がります。

参照のlifetime

HashMapgetメソッドの定義を見てみましょう。 こんな風になっています:

    fn get(&self, key: &Q) -> Option<&K>;

Rust公式ドキュメントのLifetimesの項を読むと書いてあるのですが、実はこれは省略された記法で、ちゃんと省略せずに書くとこのようになります:

    fn get<'a, 'b>(&'a self, key: &'b Q) -> Option<&'a K>;

'aとか'bとか、Rustのドキュメントを読んでいると頻繁に出てくる記法ですが、難しめなので初心者が無意識に読み飛ばしてしまいがちなところです。 この'から始まる(たいてい)1文字の名前は、関数や構造体のメンバ宣言で参照を使うときはかならず意識しなければならないlifetimeに関する記号です。 基本的に、ローカル変数宣言以外で型の名前に&を付けて書くときは、必ずこの'aのような記号もいっしょに書く必要があると考えてください。 コンパイラ様のご厚意により、初心者向けの題材で出るような簡単なケースではこれが省略できるようになっていますが、Rustを書いていればすぐにこれが省略できないケースにぶち当たります。

で、この'a'bが何を意味するのか、ということですが、注目すべきは引数と戻り値のなかにある参照のうち、どれとどれが同じ'a'bを持っているか、ということです。 ここでは、第1引数の&selfと戻り値のOptionの中身の&Kが同じ'aを持っていますね。 これは、lifetimeという言葉を使って書き下すと、『&self&Kは同じlifetimeを持つよ』と言っていることになります。 しかしそもそもlifetimeという言葉の解説をしていない(むずかしいから)ので、さっきのフィボナッチ数列のケースに当てはめてさらに言い直すと、「&K(フィボの例では&i32)に対する参照は、&selfに対する参照と同じものだとみなして同時複数参照のチェックをするからよろしく」という意味になります。

これで、最初のフィボナッチ数値の例でコンパイルエラーになる原因が分かりました。 getメソッド宣言中のlifetimeにより、fib4が保持しているi32への参照は、fibへの参照と同じものとして扱われるので、fib4が生きている状態でfibへのmut参照を取ってくることはできないのです。

代わりにどうすればいいか

しかし、しかしです。 フィボナッチ数列の4つめと5つめの値を足して6つめの値を計算したいんです。 どうしてもしたいんです。 どうすればコンパイラを満足させるようなコードが書けるのでしょうか?

方法の1つとしては、fib4fib5を取る時に、参照を断ち切ってしまうという手があります。 単純に、*をつけて参照外しをしてしまうのです。

    let fib4 : i32 = *fib.get(&4).unwrap();
    let fib5 : i32 = *fib.get(&5).unwrap();
    fib.insert(6, fib4 + fib5);

先ほどの例と比べて、fib4fib5&i32型からi32型になっていることに注目してください。 参照で死ぬなら、参照なんて保持しなければええんや! というわけで、これは一つの立派な解決策です。 ただし問題があり、これではi32のコピーが発生してしまいます。 ここではi32ですからそのコストなんて微々たるものですが、これがもっとデカい型だったときには目も当てられませんし、そもそもコピーできない型だってあります。

そんな場合には、こんな解決法があります。

    let fib6 : i32 = {
        let fib4 : &i32 = fib.get(&4).unwrap();
        let fib5 : &i32 = fib.get(&5).unwrap();
        fib4 + fib5
    };
    fib.insert(6, fib6);

要するに、要件として

  • 6番目のフィボナッチ数を作るのに、4番目と5番目のフィボナッチ数は絶対に必要
  • でも、6番目のフィボナッチ数をfibに挿入する時に、fib4fib5に生きていてほしくない

ということなので、じゃあfibへの挿入と、6番目のフィボナッチ数(fib6)の作成を分けてしまえばいいじゃんってことになります。 fib6の初期化はあんまり見ない構文ですが、こういうやり方もできるということでよろしくお願いします(fib4 + fib5のあとにセミコロンがない点に注意)。 この構文の何が嬉しいかというと、fib6ができあがったあとには、fib4fib5もどちらももう死んでいることです。 なので、遠慮なくfibへのmut参照呼び出しをすることができるのです。 しかもi32のコピーが発生していません。 上の書き方に比べてほぼあらゆる面で優れていますが、読みやすさだと上のコードのほうがちょっといい気がするので、コピーのコストが問題にならないプリミティブ型なら上のコードで書いてしまっていいでしょう。