Day 005 — 所有権の基礎(ムーブセマンティクス)

2026-08-02 🟢 初心者 / Phase 1 実装 所有権の基礎(ムーブセマンティクス)

📚 背景知識(読んでから問題へ)

Day 003では「代入・関数呼び出しでムーブが起きること」、Day 004では「Vecの要素に対する部分ムーブが禁止されていること」を学びました。今日は一歩進んで、「関数に値を渡した後、その値をもう一度使いたい場合はどうするか」を考えます。

なぜ「渡したらもう使えない」のか

fn add_exclamation(msg: String) {
    println!("{}!", msg);
}

fn main() {
    let greeting = String::from("Hello");
    add_exclamation(greeting);
    println!("{}", greeting); // ← ここでエラー
}

add_exclamation(greeting)という呼び出しは、greetingが持つStringの所有権を関数の引数msgムーブします。関数を抜けるときmsgはスコープを外れてDrop(メモリ解放)されるため、呼び出し元のgreetingはもう有効な値を指していません。Rustはこれを「ムーブ済みの値を借用しようとした」エラー(E0382)としてコンパイル時に拒否します。

解決策1: Take & Return パターン

まだ&mut(可変参照)を習っていない今の時点でも、「所有権を受け取って、変換して、返す」という形にすれば同じ変数を使い続けられます。

fn add_exclamation(msg: String) -> String {
    format!("{}!", msg)
}

fn main() {
    let greeting = String::from("Hello");
    let greeting = add_exclamation(greeting); // 所有権を受け取り直す
    println!("{}", greeting); // OK
}

関数がStringを引数で受け取り、変換したStringを戻り値として返す。呼び出し側はその戻り値を同じ変数名に再束縛(シャドーイング)することで、あたかも「その場で書き換えた」かのように扱えます。これは値を消費(consume)して新しい値を生成する、関数型プログラミングに近い発想です。

Copy型ならこの問題自体が起きない

i32のようなCopy型を関数に渡した場合は話が別です。

fn increment(n: i32) -> i32 {
    n + 1
}

fn main() {
    let n = 10;
    let n2 = increment(n);
    println!("n = {}, n2 = {}", n, n2); // n はそのまま使える
}

i32Copyトレイトを実装しているため、increment(n)の呼び出しはn複製を関数に渡すだけで、元のnは影響を受けません。「渡した後も元の変数が使えるかどうか」は、その型がCopyかどうかで根本的に挙動が変わる——これがRustの所有権システムの核心です。

📝 問題

  1. デバッグ課題: 冒頭のadd_exclamation(戻り値なし版)のコードをコンパイルすると発生するエラーについて、エラーコード(E0xxx)とおおまかなメッセージ内容を予想してください
  2. 実装課題A: 次の2つの関数を実装してください
    • fn add_prefix(msg: String, prefix: &str) -> Stringmsgの先頭にprefixを付けた新しいStringを返す
    • fn add_suffix(msg: String, suffix: &str) -> Stringmsgの末尾にsuffixを付けた新しいStringを返す
  3. 実装課題B: mainの中で、let mut s = String::from("Hello");から始めて、上記2関数をTake & Returnパターンで連続して呼び出し、最終的に"[LOG] Hello [END]"という文字列を作ってください
  4. 比較実装課題: fn increment(n: i32) -> i32 を実装し、increment呼び出し後も元のi32変数がそのまま使えることを確認するコードを書いてください。「なぜStringはTake & Returnが必要でi32は不要なのか」をコメントで1〜2行説明してください
  5. 修正課題: 冒頭のエラーになるコードを、Take & Returnパターンを使って修正してください(add_exclamationStringを返すように変更する)

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 関数にStringを「値として」渡すと、呼び出し元の変数はその後どうなるか、Day 003の内容を思い出してください
  • 「渡した値を変換した上で、もう一度使いたい」場合、関数から何を受け取ればよいでしょうか?
  • i32Stringで、関数に渡した後の挙動がなぜ違うのか、型がどんなトレイトを実装しているかに注目してください
ヒント2 — アプローチ
  • 冒頭のエラーは「ムーブ済みの値を使おうとした」ことを示すエラーコードです(E03xx系)
  • add_prefixadd_suffixはどちらもStringを引数で受け取り、format!マクロで新しいStringを組み立てて返すだけです
  • main側ではs = add_prefix(s, "...")のように同じ変数へ再代入することで、所有権を受け取り直しながら値を更新していく流れを作れます
  • incrementn + 1を返すだけの単純な関数です。i32Copyトレイトを実装しているため、関数に渡しても元の変数の所有権は移動しません
ヒント3 — コード骨格
fn add_prefix(msg: String, prefix: &str) -> String {
    // format!("{}{}", prefix, msg) を返す
}

fn add_suffix(msg: String, suffix: &str) -> String {
    // format!("{}{}", msg, suffix) を返す
}

fn increment(n: i32) -> i32 {
    n + 1
}

fn add_exclamation(msg: String) -> String {
    // format!("{}!", msg) を返す
}

fn main() {
    let mut s = String::from("Hello");
    s = add_prefix(s, "[LOG] ");
    s = add_suffix(s, /* ここに suffix を渡す */);
    println!("{}", s);

    let n = 10;
    let n2 = increment(n);
    // n はここでも使える。理由をコメントで書く

    let greeting = String::from("Hello");
    let greeting = add_exclamation(greeting);
    println!("{}", greeting);
}

模範解答

fn add_prefix(msg: String, prefix: &str) -> String {
    format!("{}{}", prefix, msg)
}

fn add_suffix(msg: String, suffix: &str) -> String {
    format!("{}{}", msg, suffix)
}

fn increment(n: i32) -> i32 {
    n + 1
}

fn add_exclamation(msg: String) -> String {
    format!("{}!", msg)
}

fn main() {
    // --- 実装課題A・B: Take & Return パターンでStringを変換チェーンする ---
    let mut s = String::from("Hello");
    s = add_prefix(s, "[LOG] "); // sの所有権をadd_prefixへ渡し、戻り値を再度sへ束縛
    s = add_suffix(s, " [END]"); // 同様にadd_suffixへ渡して受け取り直す
    println!("{}", s); // => [LOG] Hello [END]

    println!("---");

    // --- 比較実装課題: Copy型(i32)は所有権が移動しない ---
    let n = 10;
    let n2 = increment(n);
    // i32はCopy トレイトを実装しているため、increment(n) は n の複製を渡すだけ。
    // 関数呼び出し後もnの所有権はmainに残ったままなので、そのまま使い続けられる。
    println!("n = {}, n2 = {}", n, n2); // => n = 10, n2 = 11

    println!("---");

    // --- 修正課題: Take & Returnでadd_exclamationを安全に呼び出す ---
    let greeting = String::from("Hello");
    let greeting = add_exclamation(greeting); // 戻り値を同名でシャドーイング
    println!("{}", greeting); // => Hello!
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pipeline() {
        let mut s = String::from("Hello");
        s = add_prefix(s, "[LOG] ");
        s = add_suffix(s, " [END]");
        assert_eq!(s, "[LOG] Hello [END]");
    }

    #[test]
    fn test_copy_type_unaffected_after_call() {
        let n = 10;
        let n2 = increment(n);
        assert_eq!(n, 10); // Copy型なのでincrement呼び出し後もnは無傷
        assert_eq!(n2, 11);
    }

    #[test]
    fn test_add_exclamation_take_and_return() {
        let greeting = String::from("Hello");
        let greeting = add_exclamation(greeting);
        assert_eq!(greeting, "Hello!");
    }
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
[LOG] Hello [END]
---
n = 10, n2 = 11
---
Hello!

$ cargo test
running 3 tests
test tests::test_add_exclamation_take_and_return ... ok
test tests::test_copy_type_unaffected_after_call ... ok
test tests::test_pipeline ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

🪜 Step-by-Step 解説

1
なぜ元のコードはコンパイルエラーになるのか
fn add_exclamation(msg: String) {
    println!("{}!", msg);
}

fn main() {
    let greeting = String::from("Hello");
    add_exclamation(greeting); // ここでgreetingの所有権がmsgへムーブ
    println!("{}", greeting);  // ← E0382
}
add_exclamation(greeting)の呼び出し時点で、greetingが指していたStringの所有権は引数msgにムーブされます。関数を抜けるとmsgはスコープアウトしてDropされ、そのメモリは解放されます。呼び出し元に残っているgreetingという変数名は、もう「有効な値の持ち主」ではありません。次の行でprintln!("{}", greeting)と書くと、コンパイラは「ムーブ済みの値を借用しようとしている」と判断し、コンパイルを拒否します。
2
Take & Returnパターンで書き直す
fn add_exclamation(msg: String) -> String {
    format!("{}!", msg)
}

let greeting = String::from("Hello");
let greeting = add_exclamation(greeting);
関数のシグネチャを-> Stringに変え、format!で作った新しいStringを返すようにします。呼び出し側はlet greeting = add_exclamation(greeting);と書くことで、「greetingの所有権を関数に渡し、変換後の所有権を同じ名前で受け取り直す」という一連の流れを1行で表現しています。これは新しい変数を作っているのではなく、シャドーイング(Day 001で学んだ概念)によって同じ名前を再利用しているだけです。
3
add_prefix / add_suffix によるチェーン変換
let mut s = String::from("Hello");
s = add_prefix(s, "[LOG] ");
s = add_suffix(s, " [END]");
ここではlet mut sとして可変変数にし、s = f(s, ...)という形で再代入しています。ステップごとに「sの所有権を関数に渡す→関数が新しいStringを組み立てて返す→戻り値をsに代入し直す」を繰り返すことで、あたかもsを「その場で書き換えている」かのような処理が実現できます。ムーブと再代入の組み合わせだけで、まだ借用(&mut)を一切使わずに値の変換チェーンを組めることがポイントです。
4
increment — Copy型では同じ問題が起きない理由
let n = 10;
let n2 = increment(n);
println!("n = {}, n2 = {}", n, n2);
i32Copyトレイトを実装しています。Copyを実装した型を関数に渡す操作は「ムーブ」ではなく「ビット単位の複製」として扱われるため、increment(n)を呼び出してもnはmain関数内でそのまま有効であり続けます。Stringのようにヒープ上にデータを持つ型はCopyを実装できません(複製にはヒープ確保とコピーのコストが伴うため、Rustは暗黙の複製を許さない設計になっています)。これが、StringにはTake & Returnパターンが必要でi32には不要な根本的な理由です。

💡 設計思想・なぜこう書くのか

📌
所有権はただ1つに保つ: Rustが「値を渡したら元の変数は無効になる」という一見不便に思えるルールを採用しているのは、「ある値に対して、ある時点で有効な所有者は常にただ1つ」という原則を、C++のような手動メモリ管理言語でも、GCに頼る言語でもなく、コンパイル時の静的解析だけで保証するためです。もしRustが「関数に値を渡した後も元の変数を自由に使えてよい」というルールだったら、関数側でその値を破棄(Drop)した場合、呼び出し元の変数は「解放済みのメモリを指す変数」になってしまいます。これはC/C++でよく見るuse-after-free(解放済みメモリへのアクセス)という深刻なバグのクラスそのものです。Rustはこの種のバグを型システムと所有権チェックによって「そもそもコンパイルが通らない」形で排除しています。
📌
Take & Returnは&mutの前段: 今日学んだ理由は、この安全性を犠牲にせずに「値を更新し続ける」処理を書く方法が、借用(&mut)を知らなくても存在するからです。ただし、大きなデータを何度も関数に渡して返す設計は、値のムーブ自体は基本的に低コスト(ポインタ・長さ・容量のコピーのみ)とはいえ、関数のシグネチャが冗長になりがちです。次のテーマ(借用と参照)で学ぶ&mut Stringを使えば、所有権を移動させずに「その場で書き換える」ことができ、より簡潔な設計になります。今日学んだTake & Returnパターンは、その&mutの価値を実感するための土台です。

🛑 コンパイルエラーが出た場合

冒頭の戻り値なし版add_exclamationをそのままコンパイルすると、次のようなエラーが出ます。

fn add_exclamation(msg: String) {
    println!("{}!", msg);
}

fn main() {
    let greeting = String::from("Hello");
    add_exclamation(greeting);
    println!("{}", greeting);
}
error[E0382]: borrow of moved value: `greeting` --> src/main.rs:8:20 | 6 | let greeting = String::from("Hello"); | -------- move occurs because `greeting` has type `String`, which does not implement the `Copy` trait 7 | add_exclamation(greeting); | -------- value moved here 8 | println!("{}", greeting); | ^^^^^^^^ value borrowed here after move

読み方:

  • error[E0382]: 「ムーブ済みの値を借用しようとした」エラー。所有権が既に別の場所へ移動した後の変数を、読み取ろうとしたときに出ます
  • move occurs because ... does not implement the Copy trait: StringCopyを実装していないため、add_exclamation(greeting)の呼び出しがムーブとして扱われたことを説明しています
  • value moved here / value borrowed here after move: それぞれ「ここでムーブが起きた」「その後ここで使おうとした」という2箇所を対応付けて示してくれます

修正方法: 「模範解答」セクションのように、add_exclamationStringを返すように変更し、呼び出し側でlet greeting = add_exclamation(greeting);と受け取り直します。

🌐 他言語との比較

観点RustGoJavaC++TypeScript
関数に値を渡した後、元の変数は使えるかCopy型はムーブされ、元の変数は無効になる。Copy型(i32等)は複製されるので無効にならない値渡しは常にコピー(構造体は値コピー、スライス等は参照コピー)。呼び出し後も元の変数は常に使える参照型(Stringやオブジェクト)は「参照のコピー」が渡るだけで、元の変数は常に使える(GCが管理)値渡し(T)はコピーコンストラクタでコピー。std::moveで明示的にムーブすると元の変数は「有効だが未規定の状態」になる参照型は「参照のコピー」。元の変数は常に使える(GCが管理)
「渡した値を更新して返す」処理の書き方Take & Returnパターン(s = f(s))、または&mut参照(次テーマ)ポインタ(*T)を渡して関数内で書き換えるのが一般的オブジェクトの参照を渡し、内部状態を直接書き換えることが多い参照(T&)またはポインタを渡して直接書き換えるのが一般的オブジェクトの参照を渡し、内部プロパティを直接書き換えることが多い
安全性の担保方法コンパイル時の所有権チェック(use-after-freeを型システムで排除)実行時GC実行時GCプログラマの規律(std::move後のアクセスは未定義動作になりうる)実行時GC

Go/Java/TypeScriptのようにGCがある言語では「値を渡した後も元の変数を自由に使い続けられる」のが当たり前の感覚です。Rustで初めてこのE0382エラーに出会うと戸惑いやすいですが、これは「GCの代わりに、コンパイル時の所有権チェックで安全性を担保する」というRustの根本設計から来る、意図的な制約です。

🏆 実務での使いどころ

  • Builder風のメソッドチェーン: 今日学んだ「値を消費して新しい値を返す」考え方は、後のテーマで学ぶselfを消費する(fn method(self) -> Self)ビルダーパターンの土台になります。implとメソッドチェーンを学ぶDay 006以降で、この考え方がそのまま活きます
  • イテレータチェーン: vec.into_iter().map(|x| x + 1).collect()のような処理も、内部的には「所有権を受け取り、変換し、次に渡す」という今日のTake & Returnパターンと同じ思想で設計されています
  • 不変性を重視した設計: 「値を書き換える」のではなく「新しい値を作って返す」設計は、並行処理(Phase 3で学ぶArc<Mutex<T>>等)において、共有状態の書き換えによるバグを避けるための基本戦略にもつながります
  • APIシグネチャ設計の判断材料: 「呼び出し後に元のデータが必要かどうか」を考えるとき、不要ならTake & Returnやinto_iter()のようにムーブで良く、必要なら次テーマで学ぶ&mutを使う、という判断基準が今日の内容で身につきます

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
関数に値を渡せば、呼び出し元の変数も自動的に更新されると思うGo/Javaのポインタ・参照渡し、あるいはC++の参照引数(T&)に慣れているためRustでは値渡しは所有権のムーブであり、呼び出し元の変数への「参照」を渡しているわけではない。更新後の値を使いたいなら明示的に戻り値として受け取る必要がある
i32Stringで関数呼び出し後の挙動が同じだと思い込むどちらも「変数に値を渡す」という見た目上の操作が同じためi32Copyトレイトを実装しているため呼び出し後も元の変数が使えるが、StringCopyを実装していないためムーブが発生し、元の変数は無効になる。挙動の違いは型がCopyかどうかに完全に依存する
Take & Returnパターンさえ使えば、常に十分だと思うまだ&mut参照を知らない段階では、この方法しか知らないため大きなデータを何度もムーブ・返却するのは、関数シグネチャが冗長になったり、所有権の受け渡しが複雑になったりする場合がある。次テーマで学ぶ&mut参照は、所有権を移動させずにその場で書き換えられるため、多くの場面でより簡潔な設計になる

🚀 次のステップ

  • 発展: 今日実装したadd_prefixadd_suffixを、&mut Stringを引数に取る形(fn add_prefix(msg: &mut String, prefix: &str)、内部でmsg.insert_str(0, prefix)を使う)に書き換えてみてください。戻り値を受け取り直す必要がなくなることを体感できます
  • 次回予告: Day 006以降で借用と参照(& と &mut)へ本格的に進みます。「同時に複数の可変参照を持てない」という借用規則の意味と、今日のTake & Returnパターンとの使い分けを学びます

🎯 自己評価

自分の回答

気づき・メモ