Day 2: 借用チェッカー攻略 - 解答例

Exercise 1: 不変借用

解法1: 基本的な不変借用(推奨)

// 不変参照を受け取る
fn calculate_length(s: &String) -> usize {
    // sは読み取り専用
    // .len()メソッドは不変メソッドなので問題なく呼べる
    s.len()
}  // sの借用がここで終了、所有権は呼び出し側に残る

fn main() {
    let s = String::from("hello");

    // sを不変借用として渡す
    let len = calculate_length(&s);

    // sはまだ使える!所有権は移動していない
    println!("'{}' の長さは {}", s, len);
    // 出力: 'hello' の長さは 5
}

メモリレイアウト:

スタック (main):
+----------+
| s: ptr   | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |          ^
| cap: 5   |          |
+----------+          |
                      |
スタック (calculate_length):
+----------+          |
| s: &     |----------+ (mainのsへの参照)
+----------+

解法2: より汎用的な &str を使う(上級)

// &str を受け取ることで、StringとString literalの両方に対応
fn calculate_length(s: &str) -> usize {
    s.len()
}

fn main() {
    // Stringを渡す場合
    let s1 = String::from("hello");
    let len1 = calculate_length(&s1);  // &String は &str に自動変換される
    println!("'{}' の長さは {}", s1, len1);

    // String literalを渡す場合
    let s2 = "world";
    let len2 = calculate_length(s2);  // &str はそのまま渡せる
    println!("'{}' の長さは {}", s2, len2);
}

トレードオフ:

  • &String: Stringに特化、型が明確
  • &str: より柔軟、StringとString literalの両方に対応

解法3: 複数の情報を取得

// 複数の不変参照を同時に使う
fn analyze_string(s: &String) -> (usize, bool, char) {
    let len = s.len();
    let is_empty = s.is_empty();
    let first_char = s.chars().next().unwrap_or(' ');

    (len, is_empty, first_char)
}

fn main() {
    let s = String::from("hello");

    // 複数の情報を一度に取得
    let (len, empty, first) = analyze_string(&s);

    // sはまだ使える
    println!("文字列: {}", s);
    println!("長さ: {}, 空: {}, 最初の文字: {}", len, empty, first);
}

Exercise 2: 可変借用

解法1: 基本的な可変借用(推奨)

// 可変参照を受け取る
fn append_world(s: &mut String) {
    // sは可変参照なので、変更できる
    s.push_str(" world");

    // デバッグ出力(可変参照でも読み取りできる)
    println!("関数内: {}", s);
}

fn main() {
    // mutキーワードが必要
    let mut s = String::from("hello");

    // 可変借用として渡す
    append_world(&mut s);

    // 変更が反映されている
    println!("関数外: {}", s);  // "hello world"
}

メモリの変化:

呼び出し前:
スタック (main):
+----------+
| s: ptr   | -----> ヒープ: ['h']['e']['l']['l']['o']
| len: 5   |
| cap: 5   |
+----------+

append_world 実行中:
スタック (append_world):
+----------+
| s: &mut  |----------+ (mainのsへの可変参照)
+----------+          |
                      v
スタック (main):      ヒープ: ['h']['e']['l']['l']['o'][' ']['w']['o']['r']['l']['d']
+----------+                  (容量が自動拡張される)
| s: ptr   | ------>
| len: 11  |
| cap: 11  |
+----------+

解法2: 複数の可変操作

// 複数の操作を行う可変借用
fn transform_string(s: &mut String) {
    // 1. 追加
    s.push_str(" world");

    // 2. 大文字化
    *s = s.to_uppercase();

    // 3. 接尾辞を追加
    s.push_str("!");
}

fn main() {
    let mut s = String::from("hello");
    println!("元: {}", s);  // "hello"

    transform_string(&mut s);

    println!("変更後: {}", s);  // "HELLO WORLD!"
}

解法3: 条件付き変更

// 条件に応じて変更
fn append_if_short(s: &mut String, suffix: &str) -> bool {
    if s.len() < 10 {
        s.push_str(suffix);
        true  // 変更された
    } else {
        false  // 変更されなかった
    }
}

fn main() {
    let mut s1 = String::from("hi");
    let changed1 = append_if_short(&mut s1, " world");
    println!("s1: {}, 変更: {}", s1, changed1);  // "hi world", true

    let mut s2 = String::from("very long string");
    let changed2 = append_if_short(&mut s2, " world");
    println!("s2: {}, 変更: {}", s2, changed2);  // "very long string", false
}

Exercise 3: 借用ルールの違反を修正

問題のコード

// ❌ このコードはコンパイルエラー
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;      // 不変借用
    let r2 = &s;      // 不変借用(複数OK)
    let r3 = &mut s;  // エラー!不変借用と可変借用は共存できない

    println!("{}, {}, {}", r1, r2, r3);
}

エラーメッセージ:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s;
  |              -- immutable borrow occurs here
5 |     let r2 = &s;
6 |     let r3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, {}", r1, r2, r3);
  |                            -- immutable borrow later used here

解法1: スコープを分ける(基本)

fn main() {
    let mut s = String::from("hello");

    // 不変借用のスコープ
    {
        let r1 = &s;
        let r2 = &s;
        println!("{}, {}", r1, r2);
    }  // r1, r2 のスコープが終了

    // 可変借用のスコープ
    {
        let r3 = &mut s;
        r3.push_str(" world");
        println!("{}", r3);
    }  // r3 のスコープが終了

    println!("最終: {}", s);  // "hello world"
}

解法2: NLL(Non-Lexical Lifetimes)を活用(推奨)

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    // r1, r2 を使い終わる
    println!("{}, {}", r1, r2);

    // ここでr1, r2のライフタイムが終了(NLL)
    // もうr1, r2は使われないとコンパイラが判断

    let r3 = &mut s;  // OK!不変借用のライフタイムは終了している
    r3.push_str(" world");
    println!("{}", r3);

    // 最後にsを使う
    println!("最終: {}", s);
}

NLLの仕組み:

let mut s = String::from("hello");

let r1 = &s;           // r1のライフタイム開始
let r2 = &s;           // r2のライフタイム開始
println!("{}, {}", r1, r2);  // r1, r2の最後の使用
                       // r1, r2のライフタイム終了

let r3 = &mut s;       // r3のライフタイム開始(r1, r2はもう終了)
r3.push_str(" world");
println!("{}", r3);    // r3の最後の使用
                       // r3のライフタイム終了

解法3: 借用を避けて所有権を使う

fn main() {
    let s = String::from("hello");

    // 不変な参照の代わりに、clone()して独立させる
    let s_copy1 = s.clone();
    let s_copy2 = s.clone();

    println!("{}, {}", s_copy1, s_copy2);

    // 元のsを変更(可変にする)
    let mut s = s;  // シャドーイング
    s.push_str(" world");

    println!("{}", s);
}

トレードオフ:

  • メリット: 借用ルールを気にしなくて良い
  • デメリット: clone()のコスト、メモリ使用量増加

よくある間違いと修正方法

間違い1: 可変借用中に不変借用を作る

// ❌ 間違い
fn wrong_example1() {
    let mut s = String::from("hello");

    let r1 = &mut s;     // 可変借用
    let r2 = &s;         // エラー!可変借用中は不変借用できない

    println!("{}, {}", r1, r2);
}

// ✅ 修正1: スコープを分ける
fn correct_example1a() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
        r1.push_str(" world");
        println!("{}", r1);
    }  // r1が終了

    let r2 = &s;  // OK
    println!("{}", r2);
}

// ✅ 修正2: NLLを活用
fn correct_example1b() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    r1.push_str(" world");
    println!("{}", r1);  // r1の最後の使用

    let r2 = &s;  // OK: r1はもう使われない
    println!("{}", r2);
}

間違い2: 複数の可変借用

// ❌ 間違い
fn wrong_example2() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;  // エラー!可変借用は1つだけ

    println!("{}, {}", r1, r2);
}

// ✅ 修正: スコープを分ける
fn correct_example2() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
        r1.push_str(" world");
        println!("{}", r1);
    }  // r1が終了

    {
        let r2 = &mut s;
        r2.push_str("!");
        println!("{}", r2);
    }  // r2が終了

    println!("最終: {}", s);
}

間違い3: 借用中に元の値を変更

// ❌ 間違い
fn wrong_example3() {
    let mut s = String::from("hello");

    let r = &s;       // 不変借用
    s.push_str(" world");  // エラー!借用中は変更できない
    println!("{}", r);
}

// ✅ 修正1: 借用を先に終わらせる
fn correct_example3a() {
    let mut s = String::from("hello");

    {
        let r = &s;
        println!("{}", r);
    }  // rが終了

    s.push_str(" world");  // OK
    println!("{}", s);
}

// ✅ 修正2: NLLを活用
fn correct_example3b() {
    let mut s = String::from("hello");

    let r = &s;
    println!("{}", r);  // rの最後の使用

    s.push_str(" world");  // OK: rはもう使われない
    println!("{}", s);
}

最適化パス

レベル1: 基本的な借用

// 初心者向け: 動くが冗長
fn level1_process(data: &Vec<i32>) -> i32 {
    let mut sum = 0;
    for i in 0..data.len() {
        sum += data[i];
    }
    sum
}

レベル2: スライスを使う

// 中級者向け: より汎用的
fn level2_process(data: &[i32]) -> i32 {
    let mut sum = 0;
    for &item in data {
        sum += item;
    }
    sum
}

// VecだけでなくArrayも受け付ける
fn use_level2() {
    let vec_data = vec![1, 2, 3, 4, 5];
    let array_data = [1, 2, 3, 4, 5];

    println!("Vec: {}", level2_process(&vec_data));
    println!("Array: {}", level2_process(&array_data));
}

レベル3: イテレータで関数型スタイル

// 上級者向け: 簡潔で効率的
fn level3_process(data: &[i32]) -> i32 {
    data.iter().sum()
}

// または、より明示的に
fn level3_process_explicit(data: &[i32]) -> i32 {
    data.iter().copied().sum()
}

レベル4: ジェネリクスで最大限の柔軟性

// エキスパート向け: 任意の数値型に対応
use std::iter::Sum;

fn level4_process<'a, T>(data: &'a [T]) -> T
where
    T: Copy + Sum<&'a T>,
{
    data.iter().sum()
}

fn use_level4() {
    let ints = vec![1, 2, 3, 4, 5];
    let floats = vec![1.5, 2.5, 3.5];

    println!("整数の合計: {}", level4_process(&ints));
    println!("浮動小数点の合計: {}", level4_process(&floats));
}

ベンチマーク結果

use std::time::Instant;

fn benchmark_borrowing_patterns() {
    let data: Vec<i32> = (0..1_000_000).collect();

    // パターン1: 所有権を取る(clone必要)
    let start = Instant::now();
    let data_clone = data.clone();
    let sum1 = process_owned(data_clone);
    println!("所有権: {:?}", start.elapsed());
    // 結果: ~5ms (cloneのコスト)

    // パターン2: 不変借用
    let start = Instant::now();
    let sum2 = process_borrowed(&data);
    println!("借用: {:?}", start.elapsed());
    // 結果: ~2ms (cloneなし)

    // パターン3: スライス
    let start = Instant::now();
    let sum3 = process_slice(&data[..]);
    println!("スライス: {:?}", start.elapsed());
    // 結果: ~2ms (借用と同じ)

    assert_eq!(sum1, sum2);
    assert_eq!(sum2, sum3);
}

fn process_owned(data: Vec<i32>) -> i32 {
    data.iter().sum()
}

fn process_borrowed(data: &Vec<i32>) -> i32 {
    data.iter().sum()
}

fn process_slice(data: &[i32]) -> i32 {
    data.iter().sum()
}

パフォーマンス比較:

  • 所有権: 5ms(cloneのコスト)
  • 借用: 2ms(最適)
  • スライス: 2ms(借用と同じ)

実践的なユースケース

ケース1: 複数の読み取り

struct Book {
    title: String,
    author: String,
    pages: u32,
}

impl Book {
    // 複数のメソッドで不変借用を使う
    fn get_info(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }

    fn is_long(&self) -> bool {
        self.pages > 300
    }

    fn summary(&self) -> String {
        format!(
            "{} ({}ページ) - {}",
            self.get_info(),
            self.pages,
            if self.is_long() { "長編" } else { "短編" }
        )
    }
}

fn main() {
    let book = Book {
        title: "Rustプログラミング".to_string(),
        author: "山田太郎".to_string(),
        pages: 450,
    };

    // 複数回不変借用しても問題なし
    println!("{}", book.get_info());
    println!("長編? {}", book.is_long());
    println!("{}", book.summary());
}

ケース2: ビルダーパターン

struct RequestBuilder {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
}

impl RequestBuilder {
    fn new(url: String) -> Self {
        Self {
            url,
            method: "GET".to_string(),
            headers: Vec::new(),
        }
    }

    // 可変借用を使ってビルダーを更新
    fn method(&mut self, method: &str) -> &mut Self {
        self.method = method.to_string();
        self  // 自分自身の可変参照を返す
    }

    fn header(&mut self, key: &str, value: &str) -> &mut Self {
        self.headers.push((key.to_string(), value.to_string()));
        self
    }

    // 最後に所有権を消費してRequestを作る
    fn build(self) -> Request {
        Request {
            url: self.url,
            method: self.method,
            headers: self.headers,
        }
    }
}

struct Request {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
}

fn main() {
    let mut builder = RequestBuilder::new("https://api.example.com".to_string());

    // メソッドチェーン(可変借用)
    builder
        .method("POST")
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer token");

    // 最後に所有権を渡してRequestを作る
    let request = builder.build();

    println!("リクエスト: {} {}", request.method, request.url);
}

ケース3: キャッシュの実装

use std::collections::HashMap;

struct Cache {
    data: HashMap<String, String>,
}

impl Cache {
    fn new() -> Self {
        Self {
            data: HashMap::new(),
        }
    }

    // 不変借用: 読み取り
    fn get(&self, key: &str) -> Option<&String> {
        self.data.get(key)
    }

    // 可変借用: 書き込み
    fn set(&mut self, key: String, value: String) {
        self.data.insert(key, value);
    }

    // 不変借用: 統計情報
    fn stats(&self) -> (usize, usize) {
        let count = self.data.len();
        let total_size: usize = self.data.values().map(|v| v.len()).sum();
        (count, total_size)
    }
}

fn main() {
    let mut cache = Cache::new();

    // 書き込み(可変借用)
    cache.set("user:1".to_string(), "Alice".to_string());
    cache.set("user:2".to_string(), "Bob".to_string());

    // 読み取り(不変借用)
    if let Some(name) = cache.get("user:1") {
        println!("ユーザー1: {}", name);
    }

    // 統計(不変借用)
    let (count, size) = cache.stats();
    println!("エントリ数: {}, 合計サイズ: {}", count, size);
}

次のステップ

このExerciseで学んだこと:

  • 不変借用: 複数同時OK、読み取り専用
  • 可変借用: 1つだけ、変更可能
  • NLL: 参照のライフタイムは最後の使用まで
  • スコープ: 借用の終了タイミングを制御

次のDay 3では、ライフタイムについてさらに深く学びます:

  • ライフタイム注釈の書き方
  • 構造体でのライフタイム
  • 関数シグネチャでのライフタイム
  • ライフタイムの推論ルール

借用をマスターすることは、Rustの安全性と柔軟性を最大限に活用するための鍵です。頑張ってください!