参照

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

参照には 2 種類あります。

  • 共有参照: &
  • 可変参照: &mut

参照は次のルールに従います。

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

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

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

パス

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

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

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

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

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

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

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

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

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

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

生存性

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

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

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


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

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


#![allow(unused)]
fn main() {
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 が期待するほど賢くないというだけです。

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

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

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

別名付け

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

可変参照は、その先祖か子孫に他の参照が存在している時、別名を持つといいます。

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

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

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

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

関連キーワード:  参照, 生存, 可変, 所有, パス, 又貸し, 別名, 定義, ポインタ, 変数