Borrow-checking surprises - 借用チェックの驚き

Rustの借用が裏切る?「評価順」「二段階借用」「再借用」「drop」の微妙な違いでハマる4つの実例

要約

Rustの borrow-checker は基本ルールは単純でも、評価順や構文の砂糖づけ(desugar)、再借用、明示的dropなどの例外があり、直感と異なる挙動を取ることがある。この記事は代表的な4つのサンプルを分かりやすく解説する。

この記事を読むべき理由

Rustは日本でも注目が高く、安全性を武器に採用が増えている。だが現場で初心者や中級者が「なんでコンパイルエラー/通るの?」と迷う借用周りの落とし穴はコストと時間につながる。本記事は実例でその原因と対策を短く示す。

詳細解説

1) 左辺と右辺の評価順(思い込みでバグ)

// rust
fn main() {
    let mut x = 0;
    let y = &mut x;
    *y = *y + 1;
}

直感では左辺で可変参照を取った後に右辺がxを読むのは不可と思うが、Rustは右辺(rvalue)を先に評価するため問題なく動く。評価順を意識すること。

2) += とメソッド呼び出しの二相借用(two-phase borrow)

// rust
fn main() {
    let mut x = 0;
    x += x; // 表面上はOK
    // desugar: AddAssign::add_assign(&mut x, x) はコンパイルエラーになる場合がある
}

+= は構文糖で、.add_assign 呼び出し経由では「二相借用」が働き、一時的に不変参照→可変参照へ切り替わる場面がある。desugar すると二相借用が適用されないため差が出る。

3) 関数呼び出しでの再借用(reborrow)

// rust
fn id(y: &mut usize) -> &mut usize { y }
fn main() {
    let mut x = 0;
    let y = &mut x;
    let z = id(y); // 実際は再借用: id(&mut *y)
    *y = 1; // エラーにならない(zは再借用で、y自体はmoveされない)
}

関数引数への直接渡しは再借用に変換される場合があり、元の参照が「move」されず使えることがある。見た目だけで判断しない。

4) 参照の寿命と明示的dropの扱い

// rust
fn foo(a: &mut usize) -> &mut usize {
    let b = &mut *a;
    let c = &mut *b;
    // drop(b); // これはコンパイルエラーになる
    c
}

c のライフタイムは a に紐づくため一見安全だが、drop(b) は b をムーブする必要があり、borrow のルールと衝突してエラーになる。ライフタイムと値のムーブを区別すること。

実践ポイント

必要なら各例の最小再現コードやコマンド(rustcのオプション)を示すサンプルを用意する。

📌 引用元:
Borrow-checking surprises