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 の借用規則は以下の原則です。
ある値に対して、以下のいずれか一方のみが成立する
- 複数の共有参照
&Tが存在する(誰も書き換えない) - ただ一つの可変参照
&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; // 未定義動作
何が問題なのか
-
最適化の破綻: コンパイラは
aにエイリアスが無いと仮定して最適化する - 値の不整合: どちらの書き込みが最終的な値か保証できない
- 並行アクセス: マルチスレッド環境では完全に破綻
- 未定義動作(UB): C/C++ と同じ危険領域に突入
Rust の安全性の源泉は「唯一性の保持」です。ここを破った瞬間、すべての保証が失われます。
Interior Mutability が存在する理由
「&T なのに内部を書き換える RefCell や Mutex があるのはなぜ?」
これも唯一性の概念で説明できます。
Rust の原則と現実の要求
Rust のルールは
&Tを通じた変更は許されない
しかし、現実では「外から見たら不変だが、内部状態は変わる」構造が必要です(例:キャッシュ、参照カウント)。
UnsafeCell の役割
pub struct RefCell<T> {
value: UnsafeCell<T>,
borrow: Cell<BorrowFlag>,
}
UnsafeCell は、Rust の借用規則の唯一の例外を作ります。
「この型だけは
&Tを通じた変更を許す」
ただし、RefCell や Mutex が実行時に借用規則をチェックすることで、唯一性を動的に保証します。
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)
}
重要な性質
-
&mut Tは&Tに自動的に変換できる(reborrow) - ライフタイム
'aの間、元の&mutは使用不可になる -
&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が必要な理由 - ✅
RefCellやMutexが成り立つ仕組み - ✅
unsafeが危険である理由
Rust の難しさは「複雑だから」ではなく、一貫性のある強いルールを持つからです。
そしてそのルールの中心が &mut の唯一性です。
ここを理解すると、Rust という言語がクリアに見えてくると思います。
Discussion