📚 背景知識(読んでから問題へ)
Day 003では「単純な変数の代入・関数呼び出し」でのムーブを扱いました。今日はもう一歩進んで、コレクション(Vec<T>)の要素に対してムーブ規則がどう働くかを見ていきます。
Vecのインデックスアクセスは何を返しているか
vec[i]という書き方は、実はIndexトレイトのindex()メソッド呼び出しの糖衣構文で、内部的には&T(要素への参照)を返します。ところが
let item = fruits[i];
のようにletで直接受けようとすると、コンパイラは*fruits.index(i)という形で暗黙に参照外し(デリファレンス)をしてから代入しようとします。要素の型がStringのようにCopyを実装していない場合、これはその要素の所有権をVecの外へムーブする操作になります。
なぜVecはこれを許さないのか
Vec<T>は内部的に「連続したメモリ領域に、有効なTの値がすきまなく並んでいる」という不変条件(invariant)を保つことで動作しています。もしfruits[1]だけ所有権を抜き取ってしまうと、その位置は「値が存在しない穴」になります。しかしVecはその穴の存在を表現する手段を持っていません(Option<T>の配列ではなくTそのものの配列だからです)。この状態で他のインデックスアクセスやDrop(スコープを抜けるときの後片付け)が走ると、存在しない値を読み書きしてしまう未定義動作につながります。
Rustの借用チェッカーは、この危険な操作をコンパイル時に完全に検出して拒否します。これが今日のデバッグ問題の主役、E0507: cannot move out of index of Vec<T>です。
対処法は3つ
- 借用する(
&fruits[i]): 所有権を移動させず、参照だけを渡す。最も一般的な解決策 .clone()する: 要素を複製して独立した所有権を得る。元のVecはそのまま使えるVec全体をinto_iter()で消費する: 「一部だけ穴を開ける」のではなく「Vec全体を丸ごと所有権ごと消費する」なら、不変条件を壊さないので合法です
📝 問題
以下のコードにはコンパイルエラーが発生する箇所があります。
fn print_summary(item: String) {
println!("summary: {}", item);
}
fn main() {
let fruits = vec![
String::from("apple"),
String::from("banana"),
String::from("cherry"),
];
for i in 0..fruits.len() {
let item = fruits[i];
print_summary(item);
}
println!("all fruits: {:?}", fruits);
}
次の問いに答えてください。
- どの行でコンパイルエラーが発生するか。
Vecが保つ不変条件と所有権の観点から、なぜfruits[i]をそのままletで受けられないのかを説明してください - 発生するコンパイルエラーのエラーコード(
E0xxx)と、おおまかなメッセージ内容を予想してください - このコードを2通りの方法で修正してください
- 方法A:
&fruits[i]で借用し、print_summaryのシグネチャを参照を受け取る形に変更する - 方法B:
fruits[i].clone()でディープコピーし、fruits自体はそのまま使えるようにする
- 方法A:
- 追加の実装課題:
fn total_length(items: &[String]) -> usizeを実装し、fruitsの所有権を消費せずに全要素の文字数合計を求めてください(スライス&[String]を借用するAPI設計の練習です) - オプション課題: ループ後に
fruitsを一切使わなくてよい場合、for i in 0..fruits.len()ではなくfor item in fruits.into_iter()(あるいは単にfor item in fruits)と書くとエラーなくコンパイルできる理由を説明してください
🔍 ヒント(段階的開示)
ヒント1 — 方向性
fruits[i]はIndexトレイト経由で&Stringを返す糖衣構文です。それをlet item = fruits[i];のように「値として」受け取ろうとすると、コンパイラは何をしようとするでしょうか?Vec<T>が内部でどんなデータ構造(メモリレイアウト)を持っているかを思い出してください。「穴」があると何が困るでしょうか
ヒント2 — アプローチ
- 要素の型
StringはCopyを実装していないため、「値として受け取る」操作はムーブになります。しかしVecは要素の一部だけが欠けた状態を許容できないため、コンパイラはこのムーブをコンパイル時に拒否します - エラーコードは「Vecのインデックスからムーブできない」ことを意味するものです(
E05xx系) - 方法Aは
&fruits[i]という形で参照を得て、print_summaryの引数型を&Stringまたは&strに変更します - 方法Bは
.clone()を呼ぶだけで、fruits側のコードは変更不要です into_iter()はVecの所有権そのものを消費するイテレータを生成します。「一部だけ抜き取る」のではなく「全体を丸ごと明け渡す」ため、穴が生まれず合法です
ヒント3 — コード骨格
fn print_summary(item: &str) {
println!("summary: {}", item);
}
fn total_length(items: &[String]) -> usize {
// items.iter() で借用しながら各要素のlen()を合計する
items.iter().map(|s| s.len()).sum()
}
fn main() {
let fruits = vec![
String::from("apple"),
String::from("banana"),
String::from("cherry"),
];
// 方法A: 借用でループする
for i in 0..fruits.len() {
let item = &fruits[i];
print_summary(item);
}
println!("all fruits: {:?}", fruits);
println!("total length: {}", total_length(&fruits));
// オプション課題: into_iter() で丸ごと消費する版(別ブロックで確認)
let fruits2 = vec![String::from("kiwi"), String::from("mango")];
for item in fruits2.into_iter() {
print_summary(&item);
}
// ここで fruits2 を使おうとするとコンパイルエラーになる(丸ごとムーブ済みのため)
}
✅ 模範解答
fn print_summary(item: &str) {
println!("summary: {}", item);
}
fn total_length(items: &[String]) -> usize {
items.iter().map(|s| s.len()).sum()
}
fn main() {
let fruits = vec![
String::from("apple"),
String::from("banana"),
String::from("cherry"),
];
// --- 方法A: 借用でループする(fruitsの所有権は失わない) ---
for i in 0..fruits.len() {
let item = &fruits[i]; // &String を得るだけ。ムーブは発生しない
print_summary(item); // &String は &str へ自動でderef coercionされる
}
println!("---");
// ループを抜けた後も fruits は完全に有効
println!("all fruits: {:?}", fruits);
// 追加課題: total_length はスライスを借用するだけなので fruits の所有権は消費しない
println!("total length: {}", total_length(&fruits));
println!("---");
// --- 方法B: clone() でディープコピーする版(別のVecで確認) ---
let fruits_b = vec![String::from("grape"), String::from("melon")];
for i in 0..fruits_b.len() {
let item = fruits_b[i].clone(); // 要素を複製。fruits_bの所有権は無傷
print_summary(&item);
}
println!("fruits_b (still valid): {:?}", fruits_b);
println!("---");
// --- オプション課題: into_iter() で Vec 全体を消費する版 ---
let fruits2 = vec![String::from("kiwi"), String::from("mango")];
for item in fruits2.into_iter() {
// item はループのたびに所有権ごと渡ってくる(Vec全体を明け渡しているので合法)
print_summary(&item);
}
// ここで println!("{:?}", fruits2); と書くとコンパイルエラーになる
// (fruits2はinto_iter()で丸ごとムーブ済みのため、あえて書かない)
}
▶ 実行結果を見る(cargo run)
summary: apple
summary: banana
summary: cherry
---
all fruits: ["apple", "banana", "cherry"]
total length: 16
---
summary: grape
summary: melon
fruits_b (still valid): ["grape", "melon"]
---
summary: kiwi
summary: mango
🪜 Step-by-Step 解説
let item = fruits[i];がエラーになるのかfor i in 0..fruits.len() {
let item = fruits[i]; // ここでE0507
print_summary(item);
}
fruits[i]はIndexトレイトのindex(&self, index: usize) -> &Stringを呼び出す糖衣構文です。返るのは&Stringですが、let item = fruits[i];という書き方はさらにその参照を自動で*(デリファレンス)してからitemに代入しようとします。StringはCopyを実装していないため、このデリファレンス代入はムーブとして扱われます。しかしfruitsというVecはまだ生きていて、他の要素へのアクセスやDropの責任を持ち続けているため、その一部(インデックス1の要素)だけを黙って空にすることは許されません。これがE0507: cannot move out of index of Vec<String>の正体です。
error[E0507]: cannot move out of index of `Vec<String>`というエラーになります(詳細は「コンパイルエラーが出た場合」セクション参照)。
&fruits[i]で借用するlet item = &fruits[i];
先頭に&を付けるだけで、「参照を得る」操作に変わります。所有権はムーブされず、fruitsはループの中でも外でも変わらず有効なままです。print_summaryの引数型もString(所有権を要求)から&str(借用で十分)に変更しているため、呼び出し側は&itemをそのまま渡せます(&Stringは自動的に&strへderef coercionされます)。
.clone()でディープコピーするlet item = fruits_b[i].clone();
.clone()は要素を複製して独立した所有権を持つ新しいStringを作ります。元のVec(fruits_b)の要素は無傷なので、ループ後もfruits_bを丸ごと使い続けられます。ただし要素数分のヒープ確保とコピーが発生するため、パフォーマンスが重要な場面では方法Aの借用を優先すべきです。
total_length— スライス借用によるAPI設計fn total_length(items: &[String]) -> usize {
items.iter().map(|s| s.len()).sum()
}
引数を&Vec<String>ではなく&[String](スライス)で受けているのがポイントです。&[String]はVec<String>だけでなく配列やスライスからも渡せる、より柔軟なシグネチャです。所有権を要求しないため、呼び出し側(&fruits)は関数呼び出し後もfruitsを引き続き使えます。
into_iter()が合法な理由for item in fruits2.into_iter() {
print_summary(&item);
}
fruits2[i]のような部分アクセスとは違い、into_iter()はVecというコンテナ全体の所有権を一括で消費します。ループが進むたびに要素の所有権が1つずつitemへムーブされますが、この時点でfruits2という変数自体はもう存在しない(丸ごとムーブ済みで無効化されている)ため、「一部だけ穴が開いたVec」という不正な状態は決して発生しません。「部分的なムーブは危険だが、全体の所有権移動は安全」という違いが、この2つの挙動の差を生んでいます。
💡 設計思想・なぜこう書くのか
Vec<T>が部分的なムーブを禁止するのは、「ある値の所有者は常に厳密に1つ、かつその所有者が値の完全性を保証する」という所有権システムの大原則を、コレクション型にも一貫して適用しているからです。もしfruits[1]だけ黙って抜き取れてしまったら、Vecは「自分が管理している全要素が有効である」という約束を守れなくなり、次に別の要素へアクセスするコードや、スコープを抜けるときのDrop実装が、存在しない値を扱おうとして未定義動作を引き起こしかねません。structのフィールドについては、Rustは特定のフィールドだけをムーブすることを許可しています(他のフィールドが引き続き有効な限り)。これが可能なのは、構造体のフィールドはコンパイル時に個数も名前も固定されており、コンパイラが「どのフィールドがムーブ済みか」を静的に追跡できるからです。一方Vecのインデックスは実行時に決まる動的な値(iはforループの変数)なので、コンパイラは「どの要素がムーブ済みか」を静的に追跡する手段を持ちません。静的に追跡できるものは部分ムーブを許し、動的にしか分からないものは全体か借用かの二択に倒す——これがRustの一貫した設計判断です。🛑 コンパイルエラーが出た場合
元のコード(修正前)をそのままコンパイルすると、次のようなエラーが出ます。
fn main() {
let fruits = vec![String::from("apple"), String::from("banana"), String::from("cherry")];
for i in 0..fruits.len() {
let item = fruits[i]; // ここでE0507
println!("{}", item);
}
}
読み方:
error[E0507]: 「借用された内容からムーブしようとした」エラー。Indexが返す参照の先にある値を、所有権ごと取り出そうとしたときに出ますmove occurs because value has type ... does not implement the Copy trait:StringがCopyでないため、この操作がムーブとして扱われたことを説明していますhelp: consider borrowing here: rustcは「参照にすれば解決する」ことまで具体的に提案してくれます。実際、多くの場合このhelp通りに&を1つ足すだけで解決します
修正方法: 「模範解答」セクションの方法A(&で借用)、方法B(.clone())、あるいはinto_iter()でVec全体を消費するかのいずれかを状況に応じて選びます。
🌐 他言語との比較
| 観点 | Rust | Go | Java | C++ | TypeScript |
|---|---|---|---|---|---|
vec[i]で要素を取り出す挙動 | &Tを返す。値として受けるとムーブが必要になり、非Copy型はコンパイルエラー | スライスの要素アクセスは値のコピー(Goのstringは実質不変な参照型に近いので安価) | List.get(i)は常に参照のコピー(GCが管理) | operator[]は参照(T&)を返す。値として受ければコピーコンストラクタが走る | 配列アクセスは常に参照のコピー(GCが管理) |
| 要素を「抜き取って」も残りは安全か | 部分ムーブは禁止。コンパイル時にE0507で拒否される | 該当概念なし(コピーなので元のスライスは影響を受けない) | 該当概念なし(参照なのでリストは影響を受けない) | std::move(vec[i])で明示的にムーブ可能だが、その要素は「有効だが未規定の状態」として残ってしまう(バグの温床) | 該当概念なし |
| コンテナ全体を消費する操作 | into_iter()(所有権ごと消費するイテレータ) | 該当概念なし(GCがあるため「消費」の概念自体が薄い) | 該当概念なし | std::move(vec)でコンテナ自体をムーブすることは可能 | 該当概念なし |
| 安全性の担保方法 | コンパイル時の借用チェッカー | 実行時GC + コピーセマンティクス | 実行時GC | プログラマの規律(std::move後のアクセスは未定義動作になりうる) | 実行時GC |
C++との対比が特に示唆的です。C++でもstd::move(vec[i])によって要素の所有権だけを抜き取ることは技術的には可能ですが、その結果vec[i]は「有効だが未規定な状態」として残り続けます。うっかりその要素に再アクセスしてしまうバグは、実行時まで発覚しません。Rustはこの危険な操作自体を型システムレベルで存在させない(コンパイルが通らない)ことで、同じクラスのバグを設計から排除しています。
🏆 実務での使いどころ
- バッチ処理でのVec消費: キューやバッチジョブの結果を1件ずつ所有権ごと処理して破棄したい場合、
for item in vec.into_iter() { process(item); }は「処理後に元のVecへ戻る必要がない」ことをコード自体が表明する、意図の明確なイディオムです - 読み取り専用APIでのスライス活用:
total_lengthのように&[T]を引数に取る関数は、Vec<T>・配列・他のスライスのいずれからも呼び出せる汎用的なAPIになります。「所有権が要らないなら&[T]」は実務でも頻出の設計判断です - 借用チェッカーのエラーに出会ったときの判断フロー: 「①本当に所有権が必要か(不要なら借用に変える)→ ②所有権が必要だが元のデータも残したいか(
.clone())→ ③元のデータをもう使わないなら丸ごと消費してよいか(into_iter()やムーブ)」という順で検討する癖をつけると、無駄な.clone()の濫用を避けられます deref coercionの実務上の恩恵: 方法Aで&Stringを&str引数の関数にそのまま渡せたように、Rustは参照の型変換を多くの場面で自動的に行います。関数シグネチャを設計するときは、可能な限り&str・&[T]のような「受け取り側に優しい」borrowed型を選ぶのがイディオマティックです
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
vec[i]はいつでも値をコピーして返してくれると思う | Go/Java/TypeScriptなど、多くの言語でインデックスアクセスが暗黙にコピー(または参照コピー)として振る舞う経験があるため | RustではIndexトレイトが&T(参照)を返すだけであり、値として受け取ろうとすると要素の型がCopyかどうかで挙動が変わる。非Copy型はムーブが必要になり、Vecの不変条件と衝突してコンパイルエラーになる |
エラーが出たら反射的に.clone()を貼って解決しようとする | .clone()はエラーメッセージが消える「手っ取り早い」解決策に見えるため | まず「本当に所有権が必要か」を考え、借用(&)で済むなら方法Aを優先する。.clone()はヒープ確保とコピーのコストを伴う最終手段として位置づける |
for i in 0..fruits.len() { fruits[i] }とfor item in fruitsは同じ意味だと思う | どちらも「Vecの全要素を順番に処理する」という見た目上の目的が同じため | 前者はインデックスによる部分アクセス(借用かCopyでなければムーブ不可)、後者はIntoIterator実装によりVec全体の所有権を消費する処理であり、コンパイラが許可する操作の範囲が根本的に異なる |
| 構造体のフィールドの部分ムーブが許されるなら、Vecの要素の部分ムーブも許されるはずだと思う | どちらも「複合型の一部だけを取り出す」という操作に見えるため | 構造体はフィールド数・名前がコンパイル時に固定されているため、コンパイラは「どのフィールドがムーブ済みか」を静的に追跡できる。一方Vecのインデックスは実行時に決まる動的な値であり、静的追跡ができないため、部分ムーブという概念自体が禁止されている |
🚀 次のステップ
- 発展: 今回
&fruits[i]という借用を使いましたが、これは不変参照(&)です。もしループの中で要素を書き換えたい場合は可変参照(&mut)が必要になり、「同時に複数の可変参照を持てない」という借用規則が新たに関わってきます。fruits.iter_mut()を使って各要素の文字列に.push_str("!")する処理を書いてみると、次回のテーマへの橋渡しになります - 次回予告: 引き続き所有権の基礎(ムーブセマンティクス)を扱いつつ、理解が安定してきたら Day 006 前後で借用と参照(& と &mut)へ本格的に進みます(借用規則(同時可変参照の禁止)の意味)