Rustの所有権/借用周りについて混乱したので勉強がてら整理しました.
Rustの変数モデルと所有権
- Rustの変数管理モデルを値型か参照型か分類すると,値型になる.これはC++などと同じでPythonやHaskellなどとは違う.どちらかというと抽象度を犠牲にして実行速度を取るモデル.
- Rust最大の特徴は,実行速度を犠牲にしない範囲でできるだけ人間にやさしい変数管理を実現するための所有権システムを導入していること.コンパイラが変数の取り扱いを静的にチェックして値のread/writeが散らからないようにしてくれる.
所有権システムのご利益
色々言われているが,副作用の局所化なんじゃないかと思っている.例えば,下のコードのように同じ変数のmutableな参照とimmutableな参照を場所によって混ぜて利用していても,immutableな参照が生きている間は参照透過性が保証される(そういうコード以外は弾かれる):
let mut value = MyValue::new();
let mut obj = MyObj::new();
obj.impure_method(&mut value); // mutableな参照を使った計算
obj.pure_method(&value); // immutableな参照を使った計算 <- このメソッドの実行中にvalueが書き換わらないことが保証される
雑に言えばC++のconst参照にHaskellの参照透過性がついたようなもので,人間の認知負荷をかなり下げてくれる(と期待している).
所有の基本8形態
- Rustの所有権システムは変数の束縛と参照を通じて変数のread/writeの権限を管理する.
- 間接的なものまで含めると基本的な所有の形態は3種8形態に分かれる1.
- 2つの束縛: immutable/mutable bind
- 2つの参照:
&T
,&mut T
- 4つの間接参照:
Box
,Rc
,Cell
,RefCell
1. 束縛(=所有)
Rustを学んでまず出会うのは,2つの変数束縛だろう.
let x = MyStruct::new(); // immutable変数の束縛
let mut y = MyStruct::new(); // mutable変数の束縛
x
には値を変更する権限がなく, y
にはその権限がある.本当の意味での所有はこの二種類だけだ.Rustの束縛は排他的で,同時に2つ以上の名前が同じメモリアドレスの値を束縛しないことが保証されている.すでに存在する値に対して2つ目の束縛が作られる瞬間,値の型に応じてcopyかmoveのどちらかが行われる:
let z = y;
MyStruct
がCopy traitを実装している場合,copyが発生する.このとき z
に y
の値のコピーが割り当てられ,y
と z
は異なるメモリアドレスに対応することになる. そうでない場合にはmoveが発生し,単にそれ以降古い方の名前 y
を使うことができなくなる.
このように変数にアクセス可能な窓口を単一にすることで,データを破棄するタイミングが静的かつ安全に決定できる.
2. 参照(=借用)
排他的な所有の仕組みは予期しない値の変更を防ぐ上でとても役に立つが,それだけでは処理の部品を組み合わせて全体を組み立てていく現代のプログラミングに沿わない点がある:
fn foo(x: MyStruct)
{
// xを使った処理
}
fn main()
{
let x = MyStruct::new(); // Copy traitを実装していない構造体
foo(x); // xがfooの引数に束縛されたのでmoveが発生
println!("{}", x); // エラー! xという名前はもはや使えない
}
コピーすることなしに x
をもう一度使いたければ,いまのところ foo
の返り値に x
を再指定するしかない:
fn foo(x: MyStruct) -> MyStruct
{
// xを使った処理
return x; // xを返却
}
fn main()
{
let x = MyStruct::new();
let x = foo(x); // 二度のmoveを経て所有権が戻ってくる
println!("{}", x); // OK
}
しかしこのやり方はうまくない.引数がもっとたくさんあったらその分返り値をたくさん書かなければならないし, x
はimmutable変数なので変更されることがないというのにまったく並列化ができない.
借用システム: 参照
そもそもfoo
が呼ばれている間は呼び出し元の x
は変更されることがないのだから,一時的に foo
が x
を「借用」して,終わったら自動で返却するようにはできないか.
この点を補うのが参照とそれを管理する借用システムだ.Rustの参照は次のように &
か &mut
を前置して作ることができる:
let r = &x; // immutable参照の束縛
let s = &mut y; // mutable参照の束縛
Immutableな参照は参照先のreadのみが許されており,mutableな参照はそれを通して参照先のread/writeができる.どちらの場合も,*
を前につけることで参照先の値にアクセスすることができる:
*s += *r;
参照が作られている間,アクセス権がその参照へと「借用」されている.そのため一時的に元の変数のmoveやwriteができなくなる(mutable参照の場合はreadも不可)が,参照がスコープを抜けて消滅すれば所有権も元に戻る:
let x = MyStruct::new(); // Copyを実装していない構造体
{
let r = &x;
// let z = x; // NG (rが存在する間にxのmoveを試みている)
}
let z = x; // OK (rは存在しない)
面白いことに,immutable参照の再束縛はcopyになるが,mutable参照の再束縛はmoveになると定められていると考えると辻褄が合う2.これによって(自動的に)同じ変数のimmutable参照は複数存在しうるが,mutable参照は一つしか存在しないことが保証される.
借用システム: ライフタイム
借用システムが機能するためには,貸した権利の正しい返却を強制する仕組みが必要だ.返却したふりをして参照のコピーを隠し持たれると,それ以降所有者の単一性が失われてしまう.最初に挙げた例でも,返却されない参照はimmutable参照の透過性を破壊する:
obj.impure_method(&mut value); // ここでobjがmut参照をコピーして隠し持つ
obj.pure_method(&value); // このメソッドの実行中に前段のmut参照をうっかり触ってvalueを書き換える
Immutable参照であっても,返却しておかないと不正アクセスにつながるかもしれない:
let mut illegal = Illegal::new();
{
let x = MyStruct::new();
illegal.capture(&x); // 内部変数に&xをcopy
} // xは消滅
illegal.read_x(); // 内部変数をたどって存在しないはずのxのアドレスにアクセス???
このアクセス権の返却を保証する仕組みがライフタイム(寿命)だ.Rustでは変数束縛の排他性が成り立っているので,変数の寿命が尽きるのはそれを束縛する名前がなくなるときだ.そして変数束縛の寿命はスコープと呼ばれ,ブロックで管理されている:
{ // ブロックの始まり
let a = /* expression */;
// ...
} //ブロックの終わり (ここでaは消滅する)
ここまではC++と同じ状況だが,rustはライフタイムをannotateできるところと,ライフタイムが必要に応じて伸縮するところ,そしてライフタイムの一致しない参照間のcopy/moveを静的に検知して咎めてくれるところが違う.例えば,参照透過性の例では
impl<'a> MyObj<'a> { // MyObj型のlifetimeを'aとする
fn impure_method<'b>(&mut self, value: &'b mut MyValue) { // valueのlifetimeを'bとする
// ...
//self.x = x; // エラー! 異なるライフタイムラベル間で代入を試みると怒られる
}
}
のように構造体と引数で異なるlifetimeラベルを明示することで,valueへの参照がobj内にコピーされることを防ぐことができる:
obj.impure_method(&mut value); // ここで&mut valueがobjのメンバにmoveされていないことが保証される
obj.pure_method(&value); // &mut valueはもう使用されていないので,安全に&valueを使用できる
一方で,内部的に参照を保持しておきたければ両者のライフタイムを揃えれば良い.
impl<'a> MyObj<'a> { // MyObj型のlifetimeを'aとする
fn impure_method(&mut self, value: &'a mut MyValue) { // valueのlifetimeを'aに揃える
// ...
self.x = value; // (対応するメンバ変数があれば)参照を保持できる
}
}
すると,逆にその後のimmutable参照の生成を咎めることができる:
obj.impure_method(&mut value); // ここで代入した時点で&mut valueの寿命がobjのそれと揃う
obj.pure_method(&value); // エラー! objが生きている以上,&mut valueも生きており,mutable参照が生きているあいだは同じ変数の別の参照を作り出すことはできない
いずれにしても,コンパイルが通った暁には &value
が生きている間に参照先の値が書き換わらないことが保証されている.
余談だが,もし pure_method
が参照 &value
を取らないメソッドだったらどちらのケースもコンパイルが通る.ライフタイムはメソッドの実装時にannotateすることになっているので,この場合呼び出しのコードを見ただけではどちらの挙動が選択されたのかわからない:
obj.impure_method(&mut value);
obj.pure_method(); // &mut valueは使用されているかもしれないし,使用されていないかもしれない.
コードの意図を明確にするために呼び出し側がライフタイムをannotateする方法が言語レベルでサポートされていれば便利だと思うが,今ところそのような方法はないようだ3.
3. 間接参照
所有権システムと借用システムの上に実装されたスマートポインタのようなものたち.代表的にはBox
, Rc
, Cell
, RefCell
がある.役割の違いとか,網羅性とかはまだあまり良くわかっていない.
こちらがとても参考になりました.やる気が湧いたら別記事でまとめます.
-
最初の4つと後の4つはしばしば分けて紹介される.またスレッド安全に変数を管理する型として
Arc, Mutex, RwLock
があるが今回は触れない.この辺を参考にしている:https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/choosing-your-guarantees.html ↩ -
実は細かいところで違うが,そう考えておくとわかりやすい. ↩
-
obj.impure_method::<'a>(&'a mut value)
みたいな.代わりにfn assert_no_borrow<T>(_: &T) {}
のような関数を定義しておいて,mutableな借用が終わっていてほしい場所に挿入するという方法を教えてもらった. ↩