#はじめに
Rustのコンテナを使うときは、所有権を強く意識する必要があります。 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
HashMap
のget
メソッドの定義を見てみましょう。 こんな風になっています:
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つとしては、fib4
とfib5
を取る時に、参照を断ち切ってしまうという手があります。 単純に、*
をつけて参照外しをしてしまうのです。
let fib4 : i32 = *fib.get(&4).unwrap();
let fib5 : i32 = *fib.get(&5).unwrap();
fib.insert(6, fib4 + fib5);
先ほどの例と比べて、fib4
とfib5
が&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
に挿入する時に、fib4
とfib5
に生きていてほしくない
ということなので、じゃあfib
への挿入と、6番目のフィボナッチ数(fib6
)の作成を分けてしまえばいいじゃんってことになります。 fib6
の初期化はあんまり見ない構文ですが、こういうやり方もできるということでよろしくお願いします(fib4 + fib5
のあとにセミコロンがない点に注意)。 この構文の何が嬉しいかというと、fib6
ができあがったあとには、fib4
もfib5
もどちらももう死んでいることです。 なので、遠慮なくfib
へのmut
参照呼び出しをすることができるのです。 しかもi32
のコピーが発生していません。 上の書き方に比べてほぼあらゆる面で優れていますが、読みやすさだと上のコードのほうがちょっといい気がするので、コピーのコストが問題にならないプリミティブ型なら上のコードで書いてしまっていいでしょう。