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 )。

キーワード