👀

Rust の &mut を理解する!

に公開

はじめに

Rust を使っていて「なんとなく理解した気になりがち」なのが &mut です。
しかし、この一見シンプルな仕組みこそが、Rust の安全性・速度・API 設計・非同期モデルまで支える中心的な概念です。

私の主観も入っているかもしれませんが
Rust の &mut は「排他的アクセス」「最適化の基盤」「安全性の保証」を同時に実現する仕組みです。

この記事で、納得できる深さまで掘り下げていきます。

&mut は「書き換え可能」ではなく「唯一アクセス」の保証

多くの人はこう理解しています。

  • &mut は mutable な参照
  • &T は immutable な参照

しかし、これは本質を捉えていないと考えます。

正しい理解

&mut T の本質は「この値に今アクセスできるのは自分だけ」という保証です。

Rust がこの"唯一性(uniqueness)"をコンパイル時に強制することで、安全性と高速性を同時に実現しています。

言い換えると、&mut はコンパイラに対してこう宣言しています。

「この参照にはエイリアスが存在しない。だからメモリの競合を考慮せずに最適化してよい。」

これは C の restrict キーワードに近い効果を持ちますが、Rustでは型システムによって静的に検証されます。

Shared XOR Mutable の原則

Rust の借用規則は以下の原則です。

ある値に対して、以下のいずれか一方のみが成立する

  1. 複数の共有参照 &T が存在する(誰も書き換えない)
  2. ただ一つの可変参照 &mut T が存在する(他にアクセス手段がない)

この排他性により、Rust はコンパイル時にデータ競合を防ぎます。

&T&mut T の違いは「読み書き」ではなく「保証の内容」

もう少し踏み込んでみます。

  • &T → 「誰も書き換えていない」保証(複数存在可能)
  • &mut T → 「自分だけがアクセスしている」保証(排他的)

つまり、違いは能力ではなく契約の内容です。

この保証がもたらすもの

&T を持っているとき

コンパイラは次のような最適化が可能になります。

fn example(x: &i32) {
    let a = *x;
    some_function();
    let b = *x;
    // コンパイラは a == b と仮定できる(xは不変)
    // 再読み込みを省略可能
}

&mut T を持っているとき

fn example(x: &mut Vec<i32>) {
    x.push(1);
    // エイリアスが無いので、
    // Vecの内部状態の変更を安全に行える
    // 命令の並び替え最適化も可能
}

Rust の速度は、この保証によって成立しています。

標準 API が &mut self を使う理由

Rust の標準ライブラリを見ると、&mut self が頻出します。

impl<T> Vec<T> {
    fn push(&mut self, value: T) { /* ... */ }
}

trait Iterator {
    fn next(&mut self) -> Option<Self::Item>;
}

なぜ &mut が必要なのか

理由は明確で内部状態を安全に変更するには、その瞬間だけ排他的アクセスが必要だからです。

  • Vec が内部配列を拡張する場面
  • Iterator がインデックスを進める場面
  • Formatter がバッファに文字を書き込む場面

これらはすべて、外部から同時にアクセスされると破壊されます。

多くのオブジェクト指向言語では「どこからでも触れてしまうメソッド」になりがちですが、Rust は**「触ってよいのはこの呼び出しだけ」を型で表現**します。

結果として、Rust の API は意図しない破壊から守られています。

async&mut の関係

Rust の async を書いていて、こんなエラーに遭遇したことはありませんか?

error[E0499]: cannot borrow `x` as mutable more than once at a time

これは不親切なエラーではなく、Rust が安全性を守っている証拠です。

async 関数の内部構造

async 関数はコンパイル時に状態機械(Future)に変換されます。

async fn task(x: &mut i32) {
    *x += 1;
    some_async_operation().await;
    *x += 1;
}

これは内部的に次のような構造になります。

enum TaskFuture<'a> {
    State0 { x: &'a mut i32 },
    State1 { x: &'a mut i32 },
}

問題の本質

.await をまたぐと、Future の内部状態として &mut が保持されます。しかし、以下のようなコードでは問題が生じます。

async fn example() {
    let mut data = vec![1, 2, 3];
    
    for item in &mut data {
        process(item).await; // エラー: dataが既に借用されている
    }
}

イテレータが data の可変参照を保持したまま .await すると、data 全体が借用され続け、他の操作ができなくなります。

他の言語では実行時エラーになりうる状況を、Rust はコンパイル時に検出します。

Pin&mut の組み合わせ

Future が自己参照構造を形成する可能性があるため、メモリ位置が移動すると内部ポインタが無効になります。

Pin<&mut T> の意味

Pin<&mut T>

これは次のような保証を提供します。

「この値はメモリ上で移動しない。ただし、唯一性は保証されている。」

この「不動性」と「唯一アクセス」の二重保証によって、Rust は unsafe な自己参照構造を安全に扱います。

impl Future for MyFuture {
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        // selfは移動しないが、内部の変更は可能
    }
}

async が複雑に見えるのは、実は安全性を損なわないための緻密な設計が背景にあるからです。

unsafe&mut を複数作ると何が壊れるのか

Rust の安全性は「可変参照は常に 1 つ」を前提に成り立っています。

これを破ると、言語の基盤が崩れます。

let mut x = 10;
let a = &mut x;
let b = unsafe { &mut *(a as *mut i32) }; // エイリアスを作成

*a = 20;
*b = 30; // 未定義動作

何が問題なのか

  1. 最適化の破綻: コンパイラは a にエイリアスが無いと仮定して最適化する
  2. 値の不整合: どちらの書き込みが最終的な値か保証できない
  3. 並行アクセス: マルチスレッド環境では完全に破綻
  4. 未定義動作(UB): C/C++ と同じ危険領域に突入

Rust の安全性の源泉は「唯一性の保持」です。ここを破った瞬間、すべての保証が失われます。

Interior Mutability が存在する理由

&T なのに内部を書き換える RefCellMutex があるのはなぜ?」

これも唯一性の概念で説明できます。

Rust の原則と現実の要求

Rust のルールは

&T を通じた変更は許されない

しかし、現実では「外から見たら不変だが、内部状態は変わる」構造が必要です(例:キャッシュ、参照カウント)。

UnsafeCell の役割

pub struct RefCell<T> {
    value: UnsafeCell<T>,
    borrow: Cell<BorrowFlag>,
}

UnsafeCell は、Rust の借用規則の唯一の例外を作ります。

「この型だけは &T を通じた変更を許す」

ただし、RefCellMutex実行時に借用規則をチェックすることで、唯一性を動的に保証します。

let cell = RefCell::new(5);
let a = cell.borrow_mut();
let b = cell.borrow_mut(); // パニック: 既に借用されている

Cell<T> との違い

  • Cell<T>: T: Copy の場合に使用。借用を作らず値をコピー
  • RefCell<T>: 借用を作り、実行時チェックで唯一性を保証

Interior Mutability は「例外」ではなく、Rust の哲学を維持しながら現実の要求に応える設計です。

ライフタイムと &mut の関係

&mut を完全に理解するには、ライフタイムの理解が不可欠です。

fn example<'a>(x: &'a mut i32) -> &'a i32 {
    *x += 1;
    &*x // &mut から &への変換(reborrow)
}

重要な性質

  1. &mut T&T に自動的に変換できる(reborrow)
  2. ライフタイム 'a の間、元の &mut は使用不可になる
  3. &mut のライフタイムは「最後に使われた場所」まで(Non-Lexical Lifetimes)
let mut data = vec![1, 2, 3];
let r = &mut data;
r.push(4);
// ここで r のライフタイムが終わる(NLL)

println!("{:?}", data); // OK: dataが再び使える

まとめ:&mut を理解すると Rust の全体像が見えてくる

Rust の学習で最も価値があることは、仕組みの裏にある設計思想を理解することです。

&mut の唯一性を起点にすると、以下が繋がります。

  • ✅ Rust がデータ競合を防げる理由
  • ✅ API が壊れにくい設計になっている理由
  • async が複雑に見える理由
  • Pin が必要な理由
  • RefCellMutex が成り立つ仕組み
  • unsafe が危険である理由

Rust の難しさは「複雑だから」ではなく、一貫性のある強いルールを持つからです。

そしてそのルールの中心が &mut の唯一性です。

ここを理解すると、Rust という言語がクリアに見えてくると思います。

参考資料

Discussion