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の安全性と柔軟性を最大限に活用するための鍵です。頑張ってください!