このページの目次

参照と借用

Rustの借用チェッカーが認めるルールは1つだけ:同時に、可変参照1つ、または不変参照複数、どちらか一方のみ(エイリアシングXOR変更)。NLLはチェック粒度を字句スコープから実際の使用期間へ細分化し、reborrowによりネストされた借用に明示的な注釈が不要になる——これら3つが組み合わさり、Rustの「GCなしのメモリ安全性」を構成している。

& vs &mut

let mut s = String::from("hello");
let r1 = &s;                         // 不変借用 (共有参照)
let r2 = &s;                         // 複数の不変借用が可能
println!("{} {}", r1, r2);           // OK

let r3 = &mut s;                     // 可変借用: sがmutとして宣言されている必要がある
// let r4 = &s;                      // ERROR: &mutが既に存在するため、&は作成できない
r3.push('!');

核心ルール——⁠エイリアシングXOR変更⁠:同時に、⁠複数の共有参照⁠(読み取り専用)またはちょうど1つの可変参照⁠(書き込み可能)のどちらか一方のみが存在し、両者は共存できない。このルールはデータ競合とイテレータの無効化を防ぐ。

Borrow Checkerの動作

コンパイラはMIR(Mid-level IR)上で借用チェックを行う。各借用が作成される際に、どの変数を借用したか、sharedかmutableか、いつまで生存するかを記録する。

let mut v = vec![1, 2, 3];
let first = &v[0];                   // vの不変借用開始
v.push(4);                           // ERROR: vの可変借用 — firstはまだ生存中
println!("{}", first);               // firstの最終使用箇所
// firstの借用はここで終了

エラーの原因:v.push(4)&mut v を必要とするが、first はまだスコープ内にあり &v を保持している。borrow checkerは &v&mut v の同時存在を許可できない——pushv[0] に影響を与えないことが分かっていても、borrow checkerはそれを知らない。

Non-Lexical Lifetimes (NLL、2018エディション)

NLL以前は、借用の生存期間はスコープによって決定されていた(字句的——変数は宣言から最終使用箇所までのブロック全体)。NLLでは最終使用箇所によって決定されるように変更された:

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first);               // firstの最終使用箇所
// firstの借用はここで終了(スコープの末尾ではなく!)

v.push(4);                           // OK: firstはもはや生存していないため、借用は終了している
// NLL以前では、これはコンパイルできなかった

Reborrow

&mut T を関数に渡す際、借用は「reborrow」される:

fn push(v: &mut Vec<i32>) { v.push(5); }

let mut v = vec![1, 2, 3];
push(&mut v);                        // reborrow: &mut *v → pushの &mut v
// pushの終了後、reborrowも終了 → vの所有権が回復

// 等価なコード:
// let reborrow = &mut *v;
// push(reborrow);
// drop(reborrow);  ← reborrowはここでdropされ、vが回復

Referenceのメモリレイアウト

&T&mut T は実行時に通常のポインタ(64ビット環境で8バイト)である。Borrow checkerのすべてのチェックはコンパイル時に行われ、実行時のオーバーヘッドはゼロ。

let x = 42;
let r = &x;                          // アセンブリ: lea rax, [rsp+4]; mov [rsp+8], rax

Fat pointer: &[T]&dyn Trait は16バイト——長さまたはvtableポインタが追加で格納される。

一般的なトラップ

1. 不変借用中に元の値を変更できない

let mut v = vec![1, 2, 3];
let r = &v[0];
v.clear();                           // ERROR: &v(不変)がアクティブなため、&mut vは作成できない

2. イテレータの無効化はborrow checkerによって防止される

let mut v = vec![1, 2, 3];
for item in &v {                     // forループは暗黙的に &v の借用を作成
    v.push(4);                       // ERROR: &vがアクティブなため、&mut vは作成できない
}

3. 関数が参照を返す場合——明示的なlifetimeが必要

fn first_word(s: &str) -> &str {     // lifetime elision: 実際には fn<'a>(s: &'a str) -> &'a str
    &s[..s.find(' ').unwrap_or(s.len())]
}
// 返される参照は s を借用する → コンパイラは &str が s より長く生存しないことを保証

参考

  • Rust Book: 第4章 4.2節 — 参照と借用
  • NLL RFC: RFC 2094
  • Polonius: github.com/rust-lang/polonius (次世代借用チェッカー)

Keywords: borrow checker, shared reference, mutable reference, NLL, reborrow, aliasing XOR mutation, fat pointer