// dictionary
借用チェッカー.
借用チェッカー(borrow checker)は、所有権の移動・借用の規則・ライフタイムが守られているかをコンパイル時に検査するRustコンパイラの機構です。違反したコードはコンパイルエラーとなり、実行ファイルになりません。
借用チェッカーが弾くのは、他の言語であれば実行時の不具合(解放済みメモリへのアクセス・二重解放・データ競合)として現れる問題です。検査はコンパイル時に完結するため、この安全性のために実行時のコストが増えることはありません。
fn main() {
let mut items = vec!["牛乳", "卵"];
let first = &items[0]; // items を共有借用
items.push("パン"); // エラー: E0502(共有借用が生きている間に排他借用はできない)
println!("最初の商品: {first}"); // ここで first を使うため、共有借用がまだ続いている
}Playgroundで開く何を検査するのか
借用チェッカーの検査は大きく3つに分けられます。右列は違反したときに出る代表的なエラーコードです。
| 検査項目 | 内容 | エラーコード |
|---|---|---|
| 初期化と移動 | ムーブ済み・未初期化の値を使っていないか | E0382 |
| 借用規則 | 共有借用と排他借用が共存していないか | E0502 |
| 借用規則 | 排他借用が同時に2つ以上ないか | E0499 |
| ライフタイム | 関数のローカルな値への参照を返していないか(ダングリング参照) | E0515 |
E0382・E0499・E0515 のコード例
E0382(ムーブ済みの値の使用)— 所有権が移った後の変数は使えません。
fn archive(receipt: String) {
println!("保管しました: {receipt}");
} // ここで receipt は破棄される
fn main() {
let receipt = String::from("レシート: 牛乳 200円");
archive(receipt); // 引数への受け渡しで所有権がムーブする
println!("{receipt}"); // エラー: E0382(ムーブ済みの値を使っている)
}Playgroundで開くE0499(排他借用の重複)— 同じ値への排他借用は同時に1つまでです。
fn main() {
let mut balance = 1000;
let from_atm = &mut balance;
let from_app = &mut balance; // エラー: E0499(2つ目の排他借用)
*from_atm += 500;
*from_app -= 300;
println!("残高: {balance}円");
}Playgroundで開くATMとアプリの両方から同時に残高を書き換えられない、というイメージです。
E0515(ローカル変数への参照を返す)— 関数を抜けると破棄される値の参照は返せません(ダングリング参照)。
fn label() -> &'static str {
let text = String::from("特売品");
&text // エラー: E0515(ローカル変数 text への参照を返そうとしている)
} // ここで text は破棄される
fn main() {
println!("{}", label());
}Playgroundで開く実行時コストがかからない理由
借用チェッカーの検査はコンパイル時にすべて終わり、生成される機械語には借用の状態を追跡するコードが一切含まれません。サイズの決まった型Tへの参照&T・&mut Tは、実行時にはC言語のポインタと同じ「アドレスだけの値」であり、参照カウントやロックのような付随データを持ちません(&strや&[T]のようにサイズが実行時に決まる型への参照だけは、長さの情報も併せ持ちます)。
つまり借用チェッカーは、「安全性」と「速度」のどちらかを諦めるという従来の二択を、検査をコンパイル時へ前倒しすることで回避しています。
補足
GC・参照カウントとの比較
| 方式 | 検査のタイミング | 実行時の負担 |
|---|---|---|
| 借用チェッカー(Rust) | コンパイル時 | なし(検査コードは生成されない) |
| GC(Java・Goなど) | 実行時 | 到達可能性の走査・回収の一時停止 |
| 参照カウント(Swiftなど) | 実行時 | 参照の増減ごとにカウンタ更新 |
ただしRustにも、Rcのように参照カウントで所有権を共有する型や、RefCellのように借用規則を実行時に検査する型があります。これらは借用チェッカーだけでは表現できない共有が必要な場面で明示的に選ぶものであり、既定で実行時コストがかかるわけではありません。
unsafeでも借用チェッカーは止まらない
unsafeブロックは借用チェッカーを無効化しません。unsafeが追加で許可するのは生ポインタの参照外しやunsafe fnの呼び出しなどに限られ、unsafeブロック内に書いた通常の参照も変わらず検査されます。
ただし生ポインタ(*const T / *mut T)自体は借用チェッカーの追跡対象外です。そのため生ポインタを使う場合は、安全性の担保がコンパイラからプログラマの責任へ移ることになります。
検査はMIR上で行われる
借用チェックは、コンパイラが内部で使う中間表現MIR(Mid-level Intermediate Representation)の制御フローグラフ上で行われます。NLL(非レキシカルライフタイム)が借用の有効範囲を字句上のスコープから切り離せるのも、この解析がMIRの制御フローに基づいているためです。
安全なコードでも弾かれることがある
借用チェッカーは保守的で、「安全だと証明できたものだけを通す」という方針で動きます。そのため実際には安全なコードであっても、証明できなければ拒否されます。
たとえば以下は、firstとsecondが配列の異なる要素を指すことが人間には分かります。しかし借用チェッカーは配列型の要素を添字ごとに区別せず、ひとまとめのscores[_]として扱うため、同じ場所への排他借用が2つあると判断します。
fn main() {
let mut scores = [80, 95, 70];
let first = &mut scores[0];
let second = &mut scores[1]; // エラー: E0499(別の要素だが区別されない)
*first += 5;
*second += 5;
println!("{scores:?}");
}Playgroundで開くこうした場合はsplit_at_mutやget_disjoint_mutのような標準ライブラリのAPIを使うと、安全なまま目的を達成できます。判定の精度自体もコンパイラの改良とともに向上しており、NLL(非レキシカルライフタイム)の導入はその代表例です。
この辞書が使われているページ
backlinks 8