スマートポインタのことをスマポって略してみました。
Rustのポインタ・所有権管理は本当にくっっそ面倒もとい厳格です。 使える武器を全部知っておかないと、ちょっとしたプログラムを書くのにも苦労間違いなしです。 だいたいのチュートリアルではBox
とRc
の簡単な使い方には触れますが、意外とCell
やmut
変数の使い方に触れてるものが少ない(ような気がした)ので書きました。
(注:型を読者にわかりやすくするために、変数宣言ではlet a : i32
のように型を記入していますが、本来ほとんどが省略可能です)
mut
な参照
let mut a : i32 = 1;
{
let ref_to_a : &mut i32 = &mut a;
*ref_to_a = 32;
}
println!("a = {}", a); // a = 32
これはまぁ、普通に言語チュートリアルでも出てくるようなプログラムです。
このプログラム中にmut
キーワードが3回出てきますが、このうちどれを消してもコンパイルが通らなくなります。 それぞれのmut
の意味については、まぁなんとなくわかりますよね? でもここではいちいち書き下します。
- 最初の
mut
は、変数a
の値が変更可能であることを示します。 - 2番目の
mut
は、変数ref_to_a
は「参照先の値を変更可能な」参照型であることを示します。 - 3番目の
mut
は、変数a
から「参照先の値を変更可能な」参照をとってくることを示します。
実はこのプログラム中には、もう1ヶ所mut
を挿入できるところがあります。 ここです。
// vvv ここ!!
let mut ref_to_a : &mut i32 = &mut a;
ここにmut
を挿入すると、変数ref_to_a
は「参照先を変更可能な」変数であることを示します。 2.と3.の、「参照先の値を変更可能な」とは意味が違います。 このmut
があると、例えばこんなことができます。
let mut a : i32 = 1;
let mut b : i32 = 2;
{
let mut ref_to_a : &mut i32 = &mut a;
ref_to_a = &mut b; // ここでaの代わりにbを参照させる
*ref_to_a = 32;
}
println!("a = {}, b = {}", a, b); // a = 1, b = 32
どうでしょう。 C/C++のconst ptrの概念に慣れた人ならぱっとお分かりいただけるかと思いますが、constのない言語しかやったことのない人はちょっと混乱するかもしれません。 しかし、この区別はRustをやる上で避けて通れない区別です。 がんばってなんとなく理解してください。
mut
なBox
、mut
なRc
?
これの発展です。 参照のかわりに、Box
やRc
を使うケースではどうでしょう(Box
やRc
の意味についてはここでは書きません、その辺のドキュメントを読んでください)。 ここではRc
を例に挙げます。
やはりここでも、「参照先の値を変更可能な」ものと、「参照先を変更可能な」ものを区別して考える必要があります。
「参照先を変更可能な」Rc
の方は比較的わかりやすいです。 mut
を変数名の前に追加してやるだけです。
use std::rc::Rc;
let mut a : Rc<i32> = Rc::new(1);
a = Rc::new(32); // ここで参照先を変更している
println!("a = {}", *a); // a = 32
しかし、「参照先の値を変更可能な」Rc
というのは一筋縄ではいきません。 参照を使ったコードから類推して、Rc<>
の中にmut
を書きたくなりますが、これはコンパイルできません。
use std::rc::Rc;
let a : Rc<mut i32> = Rc::new(1);
*a = 32;
println!("a = {}", *a); // a = 32
error: expected identifier, found keyword `mut`
let a : Rc<mut i32> = Rc::new(1);
特にC/C++をやったことのある人が起こしやすい勘違いなのですが、Rustのmut
は、C++のconst
と違い、型の修飾子ではありません。 mut
が書けるのは、基本的に変数宣言のlet
の直後か、参照の&
の直後だけと覚えておきましょう(一応他にもありますが)。
ではどうすれば、Rustで合法的にRc
の参照先の値を書き換えられるのでしょうか。
Rc::make_mut
とRc::get_mut
まずは不正解の答えから行きます。 Rc
型のドキュメントを読んでいると、どうやらこの2つの関数を使えば、中身の&mut T
型にアクセスできそうな気がしてきます。 しかし実は、この2つの関数には非常に強い制約があり、あなたの期待しているものではない可能性が高いです。
どういうことかというと、どちらの関数も「そのRc
の参照カウントが1のときだけ」しか、参照先の&mut
を返してくれないのです。
use std::rc::Rc;
let mut a : Rc<i32> = Rc::new(1); // Rcを新しく作ったので、参照カウントは1
*Rc::make_mut(&mut a) += 1; // make_mutを使って、aの参照先に1を足した
println!("a = {}", *a); // a = 2
// Rcをコピーしたので、参照カウントは2
let b : Rc<i32> = a.clone();
println!("a = {}, b = {}", *a, *b); // a = 2, b = 2
// aの参照先の値に1を足したい……もちろんbの参照先の値も1増えるはず???
*Rc::make_mut(&mut a) += 1;
println!("a = {}, b = {}", *a, *b); // a = 3, b = 2 bは増えてない!
2回目にmake_mut
を呼び出したときには、1回目のときとは違いa
の参照カウントは2になっています。 すると、make_mut
はi32::clone()
を呼び、新しいRc
とその参照先を作成してしまうのです。 なんでこんな不便な仕様なの? というのはめんどくさいので触れません。 代わりにどうすればいいのかを考えましょう。
Cell
を使う
そこで登場するのがCell
です。 Cell
はBox
と同じように、他の型1個をラップするオブジェクトで、get
とset
メソッドを提供します。
let a : Cell<i32> = Cell::new(1);
a.set(32);
println!("a = {}", a.get()); // a = 32
これを見るとなんでわざわざCell
なんて必要なんだ、と思われるかもしれませんが、よくよく見てください。 a
はmut
な変数ではないのに、set
メソッドが呼び出せていますね。 Cell
型は、mut
でなくてもset
メソッドを呼び出すことができます。 つまり、さっき書こうとしていた、Rc
の参照先の値に1を足してみるコードというのは、このように書くことができるのです。
let a : Rc<Cell<i32>> = Rc::new(Cell::new(1));
(*a).set((*a).get() + 1);
println!("a = {}", (*a).get()); // a = 2
Rc
とCell
の二重構造になってややこしいと思いますか? これがRustだ。 一応ちょっとだけ簡略化できて、Rc
型がDeref
traitを実装しているおかげで、(*a).get()
のような部分はa.get()
のように書けます。
これなら、Rc
の中にmut
と書くことなく、参照先の値を変更可能なRc
を作ることができます。
さて、これだけ見ると、Cell
はただ単に普通の変数のmut
制限をゆるくしただけの「軟派な」型のように見えますが、そうではありません。 普通の変数にはできるけどCell
にはできないことというのが結構あります。
-
Cell
の中身の型はCopy
をimplしていなければならない (i.e. memcpyでコピーできなければならない) -
Cell
は参照やスマートポインタなどを含め、スレッド間でやりとりすることができない -
Cell
は中身の参照を取ってくることができない
特に2.の制約がわかりやすく強力で、Cell
型は1つのスレッドからしか読み書きされないことが保証されているので、いくつ変数のaliasが作られて何ヶ所から書き込まれようとも、データが同時書き込みや書き込み中の読み込みによって壊れてしまう心配がありません。 なので、mut
とマークされていなくてもset
を呼び出せるようになっているのです。
RefCell
を使う
しかし、Cell
の制限はちょっときつすぎます。 一番きついのは1.で、Copy
な型というのは単純な整数型や、整数型のみからなるstruct
などに限られ、HashMap
などのように内部にポインタ的なものを含むような型には使えません。
また3.の条件も地味にきついです。 例えば超長い配列をCell
に入れたとすると、get
やset
するたびにいちいちコピーが発生します。 超長い配列のうち1番目の要素だけを変更したい場合でも、いちいち全体をコピーしてきて1番目を書き換え、また全体を書き戻す……という作業が必要になります (たぶんここに最適化は効きません)。
なので、たいていの場合はそこの制限が緩和されたRefCell
を使います。 RefCell
はその名の通りCell
の中に参照型を持ってるようなもので、Cell
と比べて以下のような利点があります:
-
RefCell
の中身の型はだいたい何でもいい (Sized
) -
RefCell
は中身を参照やmut
参照で取ってくることができる (Ref
やRefMut
経由) -
RefCell
の中身を参照で取れるということは、その参照を他のスレッドに安全に渡すこともできる
なんだCell
の欠点が全部取り除かれてて最強じゃないか、と思いますが、ちょっと違うのはCell
のget
とset
メソッドが取り除かれていて、代わりに参照っぽい型のRef
型を返すborrow
と、mut
参照っぽい型のRefMut
を返すborrow_mut
メソッドで置き換えられていることです。 この2つのメソッドと戻り値には、普通の変数の&
と&mut
に似た特別なルールがあり、
-
Ref
型のオブジェクトは同時に何個でも存在できる -
RefMut
型のオブジェクトが1つでも存在すると、他のRef
型およびRefMut
型のオブジェクトは存在できない
というルールがあります。
このRefCell
が普通の変数と決定的に違う点は、このルール違反が起こった時に、普通の変数ならコンパイルエラーが出ますが、RefCell
では実行時にpanic!
して落ちるという点です。 RefCell
を使う際には、プログラマの責任でborrow_mut
の呼び出し場所をきっちり管理してやる必要が生じます。 これを多用しすぎると、くっそ面倒くさいRustでコードを書く意義が薄れてしまうので、必要のないところではできるだけ使わないようにしましょう。
ただし、これが必ず必要になる場面というのがいくつか存在します。 詳しくはstd::cell
の公式ドキュメントを読んでください。
まとめ
-
Rc
の参照先の値を変更したいときは、だいたいRc<RefCell<T>>
を使う。 かなりの頻出パターンなので覚えておきたい - 1%ぐらいの確率で
Rc<Cell<T>>
やRc::get_mut, Rc::make_mut
が使えるかもしれないので、一応ドキュメントを読んでおく - でも本当はあんまりそういうコードを書きたくない