リファレンス

このセクションでは、すべての Rust プログラムが満たさなくてはならないメモリモデルを ざっくりと見ていきます。 安全なコードは、ボローチェッカーによってこのモデルを満たしていることが静的に検証されます。 危険なコードは、ボローチェッカーの裏をかくかもしれませんが、このモデルを満たします。 この基本的なモデルを満たしている限り、より多くのプログラムがコンパイルに通るように ボローチェッカーを拡張することも可能です。

リファレンスには 2 種類があります。

  • 共有リファレンス: &
  • 可変リファレンス: &mut

リファレンスは次のルールに従います。

  • リファレンスのライフタイムが、参照先のライフタイムより長くなることはできません。
  • 可変リファレンスは、別名を持つことができません。

これだけです。これがモデルの全てです。 もちろん、別名を持つとはどういうことかを定義するべきでしょう。 別名を定義するには、パス生存という概念を定義しなくてはなりません。

これから説明するモデルは疑わしく、問題があるという点に、多くの人が同意しています。 直感的なモデルとして使うにはたぶん大丈夫ですが、望むような意味論を捉えることはできないでしょう。 ここではその点にこだわらず、のちの節で使うための概念を紹介することにします。 将来的にはこの構成は大きく変わるでしょう。TODO: 構成を変える。

パス

もし、Rust が扱うのが値だけ(ポインタはない)であれば、 すべての値はただ一つの変数か複合型に所有されることになります。 ここから所有権の木構造が自然に導かれます。 スタック自信が木のルートになり、変数が直接の子になります。 変数がフィールドを持つのであれば、それは変数の直接の子になるでしょう。

このように見ると、Rust におけるすべての値は、所有権を表す木構造のパスを持つことになります。 特に重要なのは、先祖子孫です。xy が所有しているとき、xy の先祖で、 yx の子孫です。この関係は内包的であることに注意してください。 x はそれ自身の先祖であり子孫です。

リファレンスは、単純にパスの名前と定義できます。 リファレンスを作成するということは、あるメモリアドレスに所有権の パスが存在することを宣言するということです。

悲惨なことに、スタックに存在しないデータはたくさんあり、この点も考慮しなくてはいけません。 グローバル変数やスレッドローカル変数は、単純にスタックの底に存在すると考えることができます。 (ただし、可変なグローバル変数に注意が必要です)。 ヒープにあるデータは別の問題を提起します。

もし、ヒープにある各データが、スタック上のただ一つのポインタに所有されているのだとすると、 そういうポインタを、ヒープ上の値を所有する構造体だと解釈すればよいだけです。 ヒープ上のデータを独占的に所有する型の例としては、Box, Vec, String, HashMap があります。

残念ながら、ヒープ上のデータは常に独占的に所有されているわけではありません。 例えば Rc によって、共有所有権という概念がでてきます。 値が共有所有されると、その値への一意なパスが存在しないことになります。 一意なパスが存在しない値によって、いろいろな制約が発生します。

一般に、一意ではないパスを参照できるのは、共有リファレンスだけです。 しかし、相互排他を保証するメカニズムがあれば、一時的にその値(とそしてすべての子ども)への唯一のパスを確立し、 「唯一の真の所有者」を確立できるかもしれません。 もしこれが可能なら、その値を変更できるかもしれません。 とくに、可変リファレンスを取ることができるようになります。

そのようなパスを確立するために、Rust で標準的に使われる継承可変性ではなく、 内部可変性がよく使われます。 内部可変性を持った型の例としては、Cell, RefCell, Mutex, RWLock があります。 これらの型は、実行時の制約を用いて、排他的アクセスを提供します。

この効果を使った興味深い例が Rc 自身です。もし Rc の参照カウントが 1 なら、 内部状態を変更したり、move したりしても安全です。 refcount 自体も内部可変性を使っています。

変数や構造体のフィールドに内部可変性があることを型システムに正しく伝えるには、 UnsafeCell を使います。 UnsafeCell 自身は、その値に対して内部可変の操作を安全にはしません。 正しく相互排他していることを、あなた自身が保証しなくてはなりません。

生存性

生存性 (liveness) は、この章の次の節でで詳しく説明する ライフタイム (lifetime) とは違うことに注意してください。

大雑把に言うと、リファレンスをデリファレンスできるとき、 そのリファレンスは、プログラム中のある時点で 生存している といえます。 共有リファレンスは、文字通り到達不可能な場合(たとえば、解放済みメモリやリークしてるメモリに 存在している場合)を除いて、常に生存しています。 可変リファレンスには、又貸しというプロセスがあるので、到達できても生存していないことがあります。

可変リファレンスは、その子孫を他の共有リファレンスまたは可変リファレンスに又貸しすることができます。 又貸ししたリファレンスは、派生したすべたの又貸しの有効期限が切れると、ふたたび生存することになります。 たとえば、可変リファレンスは、その参照先の一つのフィールドを指すリファレンスを又貸しすることができます。

let x = &mut (1, 2);
{
    // x のフィールドを又借りする
    let y = &mut x.0;
    // この時点で y は生存しているが、x は生存していない
    *y = 3;
}
// y がスコープ外に出たので、x がふたたび生存中になる
*x = (5, 7);

複数の可変リファレンスに又貸しすることも可能ですが、その複数のリファレンスは互いに素でなくてはいけません。 つまり、どのリファレンスも他のリファレンスの先祖であってはいけないということです。 Rust は、構造体のフィールドが互いに素であることを静的に証明できるので、 フィールドの又貸しが可能です。

let x = &mut (1, 2);
{
    // x を 2 つの互いに素なフィールドに又貸しする
    let y = &mut x.0;
    let z = &mut x.1;

    // y と z は生存しているが、x は生存していない
    *y = 3;
    *z = 4;
}
// y と z がスコープ外に出たので、x がふたたび生存中になる
*x = (5, 7);

ただし、多くの場合 Rust は十分に賢くないので、複数の借り手が互いに素であることを証明できません。 これはそのような又貸しが禁じられているという意味ではなく、 単に Rust が期待するほど賢くないというだけです。

話を単純にするために、変数をリファレンス型の一種、所有中リファレンス、と仮定してみましょう。 所有中リファレンスは、可変リファレンスとほとんど同じ意味を持ちます。 可変リファレンスまたは共有リファレンスに又貸しでき、それによって生存中ではなくなります。 生存中の所有中リファレンスは、値を解放(move out)できるという特殊な性質があります (とはいえ、可変リファレンスは値をスワップアウトできますが)。 この能力は、生存中の 所有中リファレンスのみに与えられています。 そうでなければ、早すぎるタイミングでその他のリファレンスを無効にすることになります。

不適切な値の変更を lint が検出するので、mut とマークされた変数だけが変更可能なように貸し出されます。

Box がまさに所有中リファレンスのように振る舞うというおとを覚えておくと良いでしょう。 Box は値を解放することができ、変数が解放された時と同様に Rust はそのパスについて推論するための 十分な情報を持っています。

別名付け

生存性とパスを定義したので、ようやく別名を適切に定義できます。

可変リファレンスは、その先祖か子孫に他のリファレンスが存在している時、別名を持つといいます。

(二つの生存中のリファレンスが互いの別名になっている、と言うこともできます。 意味上の違いは特にありませんが、プログラムの構造の健全性を検証する時には、 この考え方の方がわかりやすいでしょう。)

これだけです。すげーわかりやすいですよね? この定義に必要なすべての用語を定義するのに 2 ページ必要に なりましたが・・・。すごく、分かりやすい。でしょ?

実際には、もう少し複雑です。リファレンスに加えて Rust には生のポインタもあります。 *const T*mut T のことです。 生のポインタには、継承可能な所有権も別名という概念もありません。 そのため、Rust は生のポインタを追跡する努力を一切しませんし、名前のポインタは極めて危険です。

生のポインタが別名という意味をどの程度持っているのか、というのはまだ答えの出てない問題です。 しかし、この節で出てきた定義が健全であるためには、生のポインタを使うとある種の生存パスが わからなくなるということ重要です。