Day 1: 所有権マスター - 解答例
Exercise 1: 所有権の移動
解法1: clone() を使う
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深いコピーを作成
// clone()により、s1とs2はそれぞれ独立したStringを所有
// s1: ヒープ上の"hello"を指すポインタを持つ
// s2: 別のヒープ領域にコピーされた"hello"を指すポインタを持つ
println!("s1 = {}, s2 = {}", s1, s2);
}
メモリレイアウト:
スタック:
+--------+ ヒープ:
| s1: ptr| --> [h][e][l][l][o]
+--------+
| s2: ptr| --> [h][e][l][l][o] (別のメモリ領域)
+--------+
トレードオフ:
- 利点: 両方の変数が独立して使える
- 欠点: ヒープメモリを2倍消費し、コピーのコストがかかる
- 適用場面: 元の値を保持したまま、新しい値を作りたい場合
解法2: 参照を使う
fn main() {
let s1 = String::from("hello");
let s2 = &s1; // 借用(参照)
// s1: Stringの所有者
// s2: s1への参照(所有権なし)
println!("s1 = {}, s2 = {}", s1, s2);
}
メモリレイアウト:
スタック:
+--------+ ヒープ:
| s1: ptr| --> [h][e][l][l][o]
+--------+ ^
| s2: ptr|-------+ (s1を指す参照)
+--------+
トレードオフ:
- 利点: メモリコピーなし、高速
- 欠点: s2はs1が生きている間しか使えない
- 適用場面: 読み取り専用でデータにアクセスしたい場合
解法3: Rc を使う(上級編)
use std::rc::Rc;
fn main() {
let s1 = Rc::new(String::from("hello"));
let s2 = Rc::clone(&s1); // 参照カウントのクローン(データはコピーしない)
// s1とs2は同じデータへの参照を共有
// 参照カウント: 2
println!("s1 = {}, s2 = {}", s1, s2);
println!("参照カウント: {}", Rc::strong_count(&s1));
}
トレードオフ:
- 利点: データのコピーなし、複数の所有者が持てる
- 欠点: 参照カウントのオーバーヘッド、スレッド間では使えない
- 適用場面: 複数の所有者が必要で、シングルスレッドの場合
Exercise 2: 関数への所有権渡し
解法1: 所有権を移動して返す(基本パターン)
// 所有権を受け取り、変換し、所有権を返す
fn take_ownership(s: String) -> String {
// s: この関数がStringの所有者
// to_uppercase()は新しいStringを作成して返す
s.to_uppercase()
// sは関数の終わりで破棄される(が、返す前に所有権が移動)
}
fn main() {
let s = String::from("hello");
// sの所有権がtake_ownershipに移動
let s = take_ownership(s);
// 戻り値で新しい所有権を受け取る
println!("{}", s); // HELLO
}
所有権の流れ:
main の s → take_ownership の s → main の s (新しい値)
"hello" "hello" "HELLO"
解法2: 借用を使う(推奨パターン)
// 参照を受け取り、新しいStringを返す
fn to_upper(s: &str) -> String {
// s: 借用(所有権なし)
s.to_uppercase()
}
fn main() {
let s = String::from("hello");
// sを借用として渡す
let upper = to_upper(&s);
// sはまだ使える!
println!("元: {}, 大文字: {}", s, upper);
}
トレードオフ:
- 利点: 元の値を保持できる、より柔軟
- 適用場面: 元の値も後で使う可能性がある場合
解法3: in-place変更(可変参照)
// 可変参照を受け取り、その場で変更
fn make_uppercase(s: &mut String) {
// s: 可変借用
*s = s.to_uppercase();
}
fn main() {
let mut s = String::from("hello");
// 可変借用として渡す
make_uppercase(&mut s);
// sの中身が変更されている
println!("{}", s); // HELLO
}
トレードオフ:
- 利点: メモリ効率が良い、新しいStringを作らない
- 欠点: 元の値が上書きされる
- 適用場面: メモリ効率を重視し、元の値が不要な場合
Exercise 3: Copy trait
完全な実装例
fn main() {
// ========== Part 1: Copy trait を持つ型 ==========
// i32 は Copy trait を実装している
// スタック上のデータをそのままコピーできる
let x = 5;
let y = x; // ビット単位のコピー
// 両方が独立した値を持つ
println!("x = {}, y = {}", x, y); // x = 5, y = 5
// xを変更してもyには影響しない
let mut x = 5;
let y = x;
x = 10;
println!("x = {}, y = {}", x, y); // x = 10, y = 5
// ========== Part 2: Copy を持たない型 ==========
// String は Copy を実装していない
// ヒープ上のデータを持つため、コピーは高コスト
let s1 = String::from("hello");
let s2 = s1; // Move: s1の所有権がs2に移動
// s1は無効になる
// println!("{}", s1); // コンパイルエラー!
// エラーメッセージ: borrow of moved value: `s1`
println!("{}", s2); // OK: s2が所有者
// ========== Part 3: 両方使いたい場合 ==========
// 両方使いたい場合は clone()
let s3 = String::from("world");
let s4 = s3.clone(); // 明示的なコピー
// 両方使える(独立した2つのStringが存在)
println!("s3 = {}, s4 = {}", s3, s4);
// ========== Part 4: なぜこの違いがあるのか ==========
// Copy可能な型の条件:
// 1. スタック上に完全に格納できる
// 2. ヒープなどの外部リソースを持たない
// 3. Dropトレイトを実装していない
// Copy可能な型の例
let numbers = [1, 2, 3, 4, 5]; // 固定長配列
let numbers_copy = numbers; // Copy
println!("元: {:?}, コピー: {:?}", numbers, numbers_copy);
// Copy不可能な型の例
let vec = vec![1, 2, 3, 4, 5]; // 動的配列(ヒープ使用)
let vec2 = vec; // Move
// println!("{:?}", vec); // エラー!
println!("vec2: {:?}", vec2);
}
より詳細な例:カスタム型でのCopy
// Copy可能な構造体
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
// Copy不可能な構造体(Stringフィールドを持つ)
#[derive(Debug)]
struct Person {
name: String, // Stringは Copy ではない
age: u32,
}
fn main() {
// Point は Copy
let p1 = Point { x: 5, y: 10 };
let p2 = p1; // Copy
println!("p1: {:?}, p2: {:?}", p1, p2); // 両方使える
// Person は Copy ではない
let person1 = Person {
name: String::from("Alice"),
age: 30,
};
let person2 = person1; // Move
// println!("{:?}", person1); // エラー!
println!("{:?}", person2);
// Personを両方使いたい場合はCloneを実装
}
// Clone を実装したPerson
#[derive(Clone, Debug)]
struct CloneablePerson {
name: String,
age: u32,
}
fn demonstrate_clone() {
let person1 = CloneablePerson {
name: String::from("Bob"),
age: 25,
};
let person2 = person1.clone(); // 明示的なクローン
println!("{:?}, {:?}", person1, person2); // 両方使える
}
よくある間違いと修正方法
間違い1: 移動後の値を使おうとする
// ❌ 間違い
fn wrong_example1() {
let s = String::from("hello");
let t = s; // sの所有権がtに移動
println!("{}", s); // コンパイルエラー!
}
// ✅ 修正1: cloneを使う
fn correct_example1a() {
let s = String::from("hello");
let t = s.clone();
println!("{}", s); // OK
}
// ✅ 修正2: 借用を使う(推奨)
fn correct_example1b() {
let s = String::from("hello");
let t = &s; // 借用
println!("{}", s); // OK
}
間違い2: 関数に渡した後に使おうとする
// ❌ 間違い
fn consume(s: String) {
println!("{}", s);
}
fn wrong_example2() {
let s = String::from("hello");
consume(s); // sの所有権が移動
println!("{}", s); // コンパイルエラー!
}
// ✅ 修正1: 所有権を返す
fn consume_and_return(s: String) -> String {
println!("{}", s);
s // 所有権を返す
}
fn correct_example2a() {
let s = String::from("hello");
let s = consume_and_return(s);
println!("{}", s); // OK
}
// ✅ 修正2: 借用を使う(推奨)
fn borrow_only(s: &str) {
println!("{}", s);
}
fn correct_example2b() {
let s = String::from("hello");
borrow_only(&s);
println!("{}", s); // OK
}
間違い3: 不要なclone()
// ❌ パフォーマンスに悪い
fn inefficient_example() {
let s = String::from("hello");
// sをこの後使わないのにclone()している
process_string(s.clone());
// sはもう使わない
}
fn process_string(s: String) {
println!("{}", s);
}
// ✅ 改善版
fn efficient_example() {
let s = String::from("hello");
// 所有権を渡す(cloneなし)
process_string(s);
// sは使えないが、それで問題ない
}
最適化パス
レベル1: 基本的な所有権移動
// 初心者向け: 動くが最適ではない
fn level1_process(data: Vec<i32>) -> Vec<i32> {
let mut result = Vec::new();
for item in data {
result.push(item * 2);
}
result
}
レベル2: 借用を活用
// 中級者向け: 借用で効率化
fn level2_process(data: &[i32]) -> Vec<i32> {
let mut result = Vec::new();
for &item in data {
result.push(item * 2);
}
result
}
// 呼び出し側で元のデータも使える
fn use_level2() {
let data = vec![1, 2, 3, 4, 5];
let result = level2_process(&data);
println!("元: {:?}, 結果: {:?}", data, result);
}
レベル3: イテレータで関数型スタイル
// 上級者向け: ゼロコスト抽象化
fn level3_process(data: &[i32]) -> Vec<i32> {
data.iter()
.map(|&x| x * 2)
.collect()
}
// コンパイル後は手書きのループと同等のパフォーマンス
レベル4: in-place変更でメモリ効率最大化
// エキスパート向け: メモリ割り当てを最小化
fn level4_process(data: &mut [i32]) {
for item in data.iter_mut() {
*item *= 2;
}
}
// 呼び出し側
fn use_level4() {
let mut data = vec![1, 2, 3, 4, 5];
level4_process(&mut data);
// dataが直接変更される(新しいVecを作らない)
println!("{:?}", data);
}
ベンチマーク結果
use std::time::Instant;
fn benchmark_ownership_patterns() {
let data: Vec<i32> = (0..1_000_000).collect();
// パターン1: clone()を使う
let start = Instant::now();
let data_clone = data.clone();
let result1 = process_by_clone(data_clone);
println!("Clone: {:?}", start.elapsed());
// 結果: ~5ms (データのコピーコスト)
// パターン2: 借用を使う
let start = Instant::now();
let result2 = process_by_borrow(&data);
println!("Borrow: {:?}", start.elapsed());
// 結果: ~3ms (コピーなし)
// パターン3: in-place変更
let start = Instant::now();
let mut data_mut = data.clone();
process_in_place(&mut data_mut);
println!("In-place: {:?}", start.elapsed());
// 結果: ~1ms (新しいVec不要)
}
fn process_by_clone(mut data: Vec<i32>) -> Vec<i32> {
for item in &mut data {
*item *= 2;
}
data
}
fn process_by_borrow(data: &[i32]) -> Vec<i32> {
data.iter().map(|&x| x * 2).collect()
}
fn process_in_place(data: &mut [i32]) {
for item in data {
*item *= 2;
}
}
パフォーマンス比較:
- Clone: 5ms(データコピー + 処理)
- Borrow: 3ms(処理のみ、新しいVec作成)
- In-place: 1ms(最も効率的、メモリ割り当てなし)
実践的なユースケース
ケース1: 設定データの管理
#[derive(Clone, Debug)]
struct Config {
host: String,
port: u16,
timeout: u32,
}
impl Config {
// 所有権を取る(この後Configを変更する場合)
fn validate(mut self) -> Result<Config, String> {
if self.port == 0 {
return Err("ポートは0にできません".to_string());
}
self.timeout = self.timeout.max(1000); // 最低1秒
Ok(self)
}
// 借用する(読み取り専用)
fn display(&self) {
println!("{}:{} (timeout: {}ms)", self.host, self.port, self.timeout);
}
// 可変借用する(一部を変更)
fn update_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
}
}
fn main() {
let config = Config {
host: "localhost".to_string(),
port: 8080,
timeout: 500,
};
let config = config.validate().expect("設定が無効");
config.display();
}
ケース2: データ処理パイプライン
fn data_pipeline() {
let data = vec![1, 2, 3, 4, 5];
// 所有権を渡しながらパイプライン処理
let result = data
.into_iter() // 所有権を消費
.map(|x| x * 2)
.filter(|x| x > &5)
.collect::<Vec<_>>();
println!("{:?}", result);
// dataはもう使えない(所有権が移動した)
}
次のステップ
このExerciseで学んだこと:
- Move vs Copy: 型によって挙動が異なる
- Clone: 明示的なコピーが必要な場合
- 借用: 所有権を移動せずにアクセス
- パフォーマンス: 適切なパターンを選ぶことで最適化
次のDay 2では、借用についてさらに深く学びます:
- 不変借用 vs 可変借用
- 借用のルール
- ライフタイムの基礎
- 借用チェッカーのエラーを読み解く
所有権を完全に理解することは、Rustの最大の難関を越えることを意味します。頑張ってください!