TL;DR
- Rust の参照は、コンパイラが有効性・生存期間・借用ルールを保証するポインタである。
Deref、AsRef、Borrow、ToOwnedなどのトレイトで、さまざまなポインタ型が参照のように振る舞う。- スマートポインタは、それぞれの動作の意味を理解して使い分ける。
内容
https://effective-rust.com/references.html
参照とポインタ
参照は、データ構造に間接的にアクセスする手段で、データを所有する変数とは別に扱う。参照はポインタ(メモリ上のアドレスを値として持つ数値)として実装される。現代の CPU は、アドレスが有効な範囲にあることや、アラインメントされていることをポインタに求める。
C や Rust では、ポインタは指す先の型の情報を持つ。Rust ではこれを生ポインタと呼ぶが、普段は使わず、参照型やポインタ型を使う。
型 T への参照 &T は内部的にはポインタだが、コンパイラは次のことを保証する。
- 有効で、正しくアラインメントされた
Tのインスタンスを指す - 使われる期間より長く有効である(生存期間)
- 借用ルールを満たす
これにより、ダングリングポインタを防げる。&T は読み取り専用で、書き込むには可変参照 &mut T を使う。64 ビット環境では、参照は 8 バイトのポインタになる。
Box<T> を使うと、データをヒープに置ける。ヒープに置いたデータは、現在のブロックのスコープより長く生きられる。Box<T> 自体も 8 バイトのポインタである。
ポインタ関連のトレイト
Deref/DerefMut:Target型への参照を作れることを表す。コンパイラは*xを見ると必要に応じてこれを使うので、スマートポインタが通常の参照のように振る舞える。Rust の数少ない暗黙の型変換の 1 つである(項目5)。Targetが型パラメータではなく関連型なのは、1 つの型が複数のDerefを実装して、参照外しの解釈が曖昧になるのを防ぐためであるAsRef/AsMut: コンパイラの特別扱いはなく、メソッドを明示的に呼んで参照を作る。変換先を型パラメータで指定するので、複数の変換先を持てる。たとえばStringはDeref<Target = str>に加えて、AsRef<[u8]>、AsRef<Path>なども実装しているPointer:でポインタの値を表示するBorrow/BorrowMut:AsRef/AsMutと同じシグネチャを持つが、参照ではないT自身にもブランケット実装がある。そのため、Tへの参照だけでなくTの値も受け取れる。HashMap::getはK: Borrow<Q>を使っているので、キーがStringのHashMapに&strでもアクセスできるToOwned: 所有権を持つ新しいデータを作るto_ownedを提供する。Cloneを一般化したもので、&strからStringを作れる
ToOwned に関係するスマートポインタに Cow(clone-on-write)がある。借用したデータか所有したデータのどちらかを持つ enum で、変更が必要になるまでは借用のまま扱い、変更するときに ToOwned で所有するコピーを作る。
ファットポインタ
Rust には組み込みのファットポインタが 2 つあり、どちらも通常のポインタの 2 倍(64 ビット環境で 16 バイト)のサイズを持つ。
1 つ目はスライス &[T] で、連続して並んだ値の一部を指す。所有権を持たないポインタと長さで構成される。[T] 自体はインスタンス化できない抽象的な型で、具体的には配列 [T; N] やベクタ Vec<T> の一部を指す。&vector[1..3] の裏では、範囲式が Range<usize> に、インデックス式が Index トレイトの index 呼び出しに変換されている。
2 つ目はトレイトオブジェクトで、インスタンスへのポインタと vtable へのポインタで構成される。vtable には、メソッドの関数ポインタに加え、型のサイズやアラインメント、drop 関数へのポインタも入っている。
スマートポインタ
Rc<T>: 参照カウント付きのポインタで、複数の経路からアクセスするデータ構造を作れる。「各データの所有者は 1 人」という所有権のルールを外すので、循環参照によるリークが起きうる。所有しない参照を持つWeak<T>を使うと、これを緩和できる。Weakから値にアクセスするにはRcへのアップグレードが必要で、元のデータが破棄されていれば失敗するRefCell<T>: 「データを変更できるのは所有者か可変参照を持つコードだけ」というルールを緩める(内部可変性)。&selfしか受け取らないメソッドでも内部状態を変更できる。代わりに、借用数を管理するメモリが余分に要り、借用のチェックが実行時になる。try_borrowで失敗をResultとして扱うか、borrowで実行時のパニックを受け入れるかを選ぶ。TがCopyなら、より軽いCell<T>を使えるArc<T>:Rc<T>のスレッド安全版で、アトミックなカウンタを使う。中身は変更できないので、MutexやRwLockと組み合わせるMutex<T>: 一度に 1 つのスレッドだけがデータにアクセスできるようにするRwLock<T>: 書き込むスレッドがなければ、複数のスレッドの同時読み込みを許す。読み込みが多い場合はMutexより効率的である
Rc と RefCell はスレッドセーフではない。Rust の借用ルールとスレッドに関するルールにより、マルチスレッドのコードでは同期用の型を使うことが強制される。ただし、これで防げるのは状態を共有する並行処理の問題の一部だけである(項目17)。
コンパイラが拒否したコードと提案を見比べる方法もあるが、各スマートポインタの意味を理解したほうが効率的である。たとえば Rc<RefCell<Vec<T>>> は共有したベクタをまとめて変更でき、Rc<Vec<RefCell<T>>> は各要素を独立に変更できる(参考: https://doc.rust-lang.org/1.15.1/book/choosing-your-guarantees.html#composition )。