Day 2: 借用チェッカー攻略 - 解説
借用チェッカーの役割
借用チェッカー(Borrow Checker)は、Rustコンパイラの中核機能で、以下をコンパイル時に防ぎます:
- データ競合(Data Race)
- ダングリング参照(Dangling Reference)
- Use After Free
- Iterator Invalidation
これらはすべて、C/C++では実行時エラーやセキュリティ脆弱性の原因となる問題です。
借用チェッカーの仕組み
// 借用チェッカーが追跡する情報
let mut s = String::from("hello"); // 1. sの所有権が開始
let r1 = &s; // 2. r1の不変借用が開始
let r2 = &s; // 3. r2の不変借用が開始
println!("{}, {}", r1, r2); // 4. r1, r2の使用
// 5. r1, r2のライフタイムが終了
let r3 = &mut s; // 6. r3の可変借用が開始(安全)
r3.push_str(" world"); // 7. r3を使った変更
println!("{}", r3); // 8. r3の使用
// 9. r3のライフタイムが終了
NLL (Non-Lexical Lifetimes)
従来のライフタイム(Rust 2015以前)
// Rust 2015: コンパイルエラー
let mut s = String::from("hello");
let r = &s;
// rのライフタイムはスコープ全体
// (実際には使われていなくても)
s.push_str(" world"); // エラー!rがまだ生きている
println!("{}", r);
問題点: 参照のライフタイムがレキシカルスコープ({})に縛られる。
NLL(Rust 2018以降)
// Rust 2018+: コンパイル成功
let mut s = String::from("hello");
let r = &s;
println!("{}", r); // rの最後の使用
// rのライフタイム終了
s.push_str(" world"); // OK!rはもう使われない
改善点: 参照のライフタイムが「最後に使用された場所」まで。
NLLの詳細な動作
fn demonstrate_nll() {
let mut data = vec![1, 2, 3];
// ケース1: 不変借用が続く
let r1 = &data;
println!("{:?}", r1); // r1の使用
println!("{:?}", r1); // r1の使用(まだ生きている)
// r1のライフタイム終了
// ケース2: 可変借用が始められる
let r2 = &mut data; // OK: r1は終了している
r2.push(4);
// ケース3: 複雑なケース
let r3 = &data;
let r4 = &data;
// r3とr4を条件分岐で使う
if r3.len() > 0 {
println!("{:?}", r3); // r3の最後の使用
}
// r4はまだ生きている
println!("{:?}", r4); // r4の最後の使用
// ここで両方のライフタイムが終了
data.push(5); // OK
}
借用の設計原則
読者・書者問題(Readers-Writer Problem)
借用システムは、並行処理の古典的な問題「読者・書者問題」を解決します。
ルール:
- 複数の読者(不変借用): 同時に読み取りOK
- 単一の書者(可変借用): 書き込み中は他のアクセス不可
時刻 不変借用 可変借用 状態
------------------------------------------------
t0 - - 空き
t1 r1 - 読み取り中
t2 r1, r2 - 複数読み取り
t3 r1, r2, r3 - 複数読み取り
t4 - - 空き(全ての読者が終了)
t5 - w1 書き込み中
t6 - - 空き
排他制御の自動化
C++の問題:
std::vector<int> data = {1, 2, 3};
int& first = data[0]; // 参照を取得
data.push_back(4); // ベクタが再確保される可能性
// first は無効になる(ダングリング参照)
std::cout << first << std::endl; // 未定義動作!
Rustの解決:
let mut data = vec![1, 2, 3];
let first = &data[0]; // 不変借用
// data.push(4); // コンパイルエラー!
// 不変借用中は変更できない
println!("{}", first); // 安全
ライフタイムの基礎
暗黙的なライフタイム
多くの場合、ライフタイムは推論されます。
// ライフタイムは省略されている
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// 実際のライフタイム(コンパイラが推論)
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
明示的なライフタイム
複数の参照がある場合、ライフタイムを明示する必要があります。
// エラー: ライフタイムが不明
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// 正しい: ライフタイムを明示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
意味: 戻り値のライフタイムは、xとyのうち短い方と同じ。
ライフタイムの実例
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("short");
result = longest(&string1, &string2);
println!("長い方: {}", result); // OK: string2はまだ有効
}
// println!("{}", result); // エラー!string2が破棄された後
}
メモリの状態:
外側のスコープ:
+----------------+
| string1 (有効) |
| result: ? |
+----------------+
|
v
内側のスコープ:
+----------------+
| string2 (有効) |
| result: &str | -----> string2 または string1 を指す
+----------------+
|
v
スコープ終了:
+----------------+
| string1 (有効) |
| result: 無効 | -----> string2 が破棄された
+----------------+
メンタルモデル:借用の直感的理解
図書館のアナロジー
不変借用(読み取り専用):
let book = String::from("Rust Book");
let reader1 = &book; // 1人目が読む
let reader2 = &book; // 2人目も読める(コピーして読む)
let reader3 = &book; // 3人目も読める
// 誰も本を変更できない
// 全員が同じ内容を読める
可変借用(書き込み):
let mut book = String::from("Draft");
let editor = &mut book; // 編集者が独占
// editor.push_str(" - Edited"); // 編集できる
// 他の人は読めない(編集中のため)
// 編集が終わるまで待つ
所有権 vs 借用の決定木
データを使いたい
|
+-- 後で元のデータも使う?
|
No --> 所有権を移動(Move)
|
Yes --> 値を変更する?
|
No --> 不変借用(&T)
| - 複数同時OK
| - パフォーマンスが良い
|
Yes --> 可変借用(&mut T)
- 1つだけ
- 排他的アクセス
ビジュアル図解:借用のライフタイム
例1: 不変借用の重複
fn example1() {
let data = vec![1, 2, 3, 4, 5];
// ^^^^^^^^ データの所有者
let r1 = &data;
// ^^^^^ r1のライフタイム開始
let r2 = &data;
// ^^^^^ r2のライフタイム開始
let r3 = &data;
// ^^^^^ r3のライフタイム開始
println!("{:?}, {:?}, {:?}", r1, r2, r3);
// ^^ ^^ ^^ 全て使用
// ここでr1, r2, r3のライフタイムが終了
}
ライフタイムのタイムライン:
時刻 data r1 r2 r3
-------------------------------------
t0 作成 - - -
t1 有効 開始 - -
t2 有効 有効 開始 -
t3 有効 有効 有効 開始
t4 有効 有効 有効 有効 <- println!
t5 有効 終了 終了 終了 <- ライフタイム終了
t6 破棄 - - -
例2: 可変借用の排他性
fn example2() {
let mut data = vec![1, 2, 3];
// ^^^^^^ 可変な所有者
let r1 = &mut data;
// ^^^^^^^^^ r1の可変借用開始(データを独占)
r1.push(4);
// ^^^^^^^ r1を使った変更
println!("{:?}", r1);
// ^^ r1の最後の使用
// r1のライフタイム終了
let r2 = &mut data;
// ^^^^^^^^^ r2の可変借用開始(r1は終了済み)
r2.push(5);
println!("{:?}", r2);
// r2のライフタイム終了
}
ライフタイムのタイムライン:
時刻 data r1 r2
----------------------------------------
t0 [1,2,3] - -
t1 [1,2,3] 借用開始 - <- 排他的
t2 [1,2,3,4] 使用中 - <- r1.push(4)
t3 [1,2,3,4] 終了 - <- println!後
t4 [1,2,3,4] - 借用開始 <- 今度はr2が排他的
t5 [1,2,3,4,5] - 使用中 <- r2.push(5)
t6 [1,2,3,4,5] - 終了 <- println!後
よくあるエラーパターンと理由
エラー1: 可変と不変の混在
let mut s = String::from("hello");
let r1 = &s; // 不変借用
let r2 = &mut s; // エラー!
println!("{}, {}", r1, r2);
エラーメッセージ:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
理由:
- r1が不変借用している間、データは変更されないことが保証されている
- r2が可変借用を作ると、その保証が破られる
- よって、両立できない
現実世界の例:
読者がドキュメントを読んでいる間に、
編集者が同じドキュメントを編集し始めたら、
読者は混乱してしまう。
Rustはこれを防ぐ。
エラー2: 複数の可変借用
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // エラー!
println!("{}, {}", r1, r2);
エラーメッセージ:
error[E0499]: cannot borrow `s` as mutable more than once at a time
理由:
- 2つの可変借用があると、どちらからでも変更できる
- データ競合の可能性がある
- よって、1つだけに制限
データ競合の例:
// もし複数の可変借用が許されたら...
let mut data = vec![1, 2, 3];
let r1 = &mut data;
let r2 = &mut data;
// r1がベクタを再確保
r1.push(4); // ヒープが再確保される可能性
// r2は古いヒープを指している(ダングリング!)
r2.push(5); // 未定義動作
エラー3: 借用中の変更
let mut s = String::from("hello");
let r = &s;
s.push_str(" world"); // エラー!
println!("{}", r);
エラーメッセージ:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
理由:
- rが不変借用している間、sの内容は変わらない保証がある
- s.push_str()で変更すると、その保証が破られる
- rが使用される前に変更されるのは危険
借用のベストプラクティス
1. 借用のスコープを最小化
// ❌ 悪い例: 借用が長すぎる
fn bad_example() {
let mut data = vec![1, 2, 3];
let r = &data;
// ... 100行のコード ...
// rを使わない処理
// ... さらに100行 ...
println!("{:?}", r); // ようやく使う
}
// ✅ 良い例: 借用を最小化
fn good_example() {
let mut data = vec![1, 2, 3];
// ... 処理 ...
{
let r = &data;
println!("{:?}", r); // すぐ使う
} // rのスコープが終了
// dataを自由に変更できる
data.push(4);
}
2. &String より &str を優先
// ❌ 柔軟性が低い
fn process_string(s: &String) {
println!("{}", s);
}
// ✅ より汎用的
fn process_str(s: &str) {
println!("{}", s);
}
fn main() {
let string = String::from("hello");
let literal = "world";
// process_string(&string); // OK
// process_string(literal); // エラー!&strは&Stringではない
process_str(&string); // OK: &Stringは&strに自動変換
process_str(literal); // OK: &strはそのまま
}
3. 可変借用を不必要に使わない
// ❌ 読むだけなのに可変借用
fn bad_get_length(s: &mut String) -> usize {
s.len()
}
// ✅ 不変借用で十分
fn good_get_length(s: &str) -> usize {
s.len()
}
理由:
- 可変借用は排他的なので、柔軟性が低い
- 意図が明確になる(読むだけなのか、変更するのか)
- 並列処理がしやすい
4. 構造体でのライフタイム設計
// ✅ ライフタイムを明示
struct Excerpt<'a> {
text: &'a str, // textのライフタイムは'a
}
impl<'a> Excerpt<'a> {
// selfと同じライフタイム
fn get_text(&self) -> &'a str {
self.text
}
// 新しいライフタイムも可能
fn first_sentence(&self) -> &str {
self.text.split('.').next().unwrap_or("")
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt { text: &novel };
println!("抜粋: {}", excerpt.get_text());
}
パフォーマンスへの影響
ゼロコスト抽象化
借用チェックはコンパイル時に行われるため、ランタイムコストはゼロです。
// このコードは...
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
// コンパイル後は生のポインタ操作と同じアセンブリになる
// ランタイムチェックなし
ベンチマーク: 借用 vs unsafe
use std::time::Instant;
fn benchmark() {
let data: Vec<i32> = (0..10_000_000).collect();
// 安全な借用
let start = Instant::now();
let sum1 = safe_sum(&data);
let time1 = start.elapsed();
// unsafeな生ポインタ
let start = Instant::now();
let sum2 = unsafe { unsafe_sum(&data) };
let time2 = start.elapsed();
println!("安全な借用: {:?}", time1); // ~20ms
println!("unsafeポインタ: {:?}", time2); // ~20ms(同じ!)
assert_eq!(sum1, sum2);
}
fn safe_sum(data: &[i32]) -> i32 {
data.iter().sum()
}
unsafe fn unsafe_sum(data: &[i32]) -> i32 {
let ptr = data.as_ptr();
let len = data.len();
let mut sum = 0;
for i in 0..len {
sum += *ptr.add(i);
}
sum
}
結果: パフォーマンスは同じ、安全性は段違い。
セルフチェック質問
レベル1: 基礎
- NLLとは?
- 借用チェッカーは何を防ぐ?
レベル2: 応用
- このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);
let r3 = &mut s; // ?
- 答え: Yes(NLLにより、r1, r2は終了済み)
- このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
- 答え: No(不変借用と可変借用が共存)
- このコードはコンパイルできるか?
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
- 答え: No(複数の可変借用)
- 関数シグネチャを設計せよ
レベル3: 設計
fn read(s: &str) -> usize
- 文字列を変更: fn modify(s: &mut String)
- 文字列の一部を返す: fn extract<'a>(s: &'a str) -> &'a str- ライフタイムを設計せよ
struct Parser<'a> {
source: &'a str,
}
impl<'a> Parser<'a> {
fn parse(&self) -> Result<&'a str, Error> {
// sourceから一部を抽出して返す
}
}
次のステップ
Day 3では、ライフタイムについてさらに詳しく学びます:
学習ポイント:
- ライフタイム注釈の文法
- 構造体とライフタイム
- ライフタイムの推論ルール(elision rules)
- 'static ライフタイム
- 高度なライフタイムパターン
借用とライフタイムを完全に理解することで、Rustの安全性と表現力を最大限に活用できるようになります。これらはRustの最も重要な概念であり、マスターすることで他の言語では不可能な安全なコードが書けるようになります。