📚 背景知識(読んでから問題へ)
Day 017〜021では「値を生成する(Counter)」「値を加工する(Windows2<I>)」「値の集計単位を変える(RunLengthEncode<I>)」というイテレータを作ってきました。これらはすべてItemが「所有された値」または「不変参照&T」でした。
今日はもう一段難しいItem = &mut T(可変参照を1個ずつ返すイテレータ)を自作します。これは実務でも頻出のパターンです(Vec::iter_mut()やHashMap::values_mut()はまさにこの形)。しかし、自分で&mut [T]のスライスから&mut Tを1個ずつ取り出すイテレータを素朴に実装しようとすると、借用チェッカーに撥ねられるという壁に必ずぶつかります。
なぜかというと、標準のIteratorトレイトのnextシグネチャは次の通りです。
fn next(&mut self) -> Option<Self::Item>;
selfは&mut Self —— つまり「このメソッド呼び出しの間だけ有効な、短命な可変借用」です。もしSelf::Itemが「selfの中に元々入っていたフィールドが持つ、もっと長いライフタイム'aの参照」だった場合、「呼び出しごとの短命な&mut self借用」を経由して「フィールドが持つ長命な'aの参照」を取り出そうとすると、Rustは安全側に倒れて短命な方のライフタイムしか保証してくれません。この「短命な入り口から長命な参照を取り出す」ときに起きる摩擦こそが、今日のデバッグ課題の本質です。
next()を呼んだ後も前回返した&mut Tを使い続けられてしまうと、同じデータへの複数の可変参照が同時に存在する」というエイリアシング違反を未然に防ぐための正当な制約です(実際、C++やGoでは「イテレータを進めたら前に取り出した要素への参照が無効になる/不定動作になる」というバグがランタイムまで発覚しません)。📝 問題
以下は「&mut [i32]のスライスを先頭から1要素ずつ&mut i32として取り出すイテレータIterMut<'a>」を実装しようとしてコンパイルが通らないコードです。
struct IterMut<'a> {
slice: &'a mut [i32],
}
impl<'a> IterMut<'a> {
fn new(slice: &'a mut [i32]) -> Self {
IterMut { slice }
}
}
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut i32;
fn next(&mut self) -> Option<Self::Item> {
if self.slice.is_empty() {
return None;
}
let (first, rest) = self.slice.split_at_mut(1);
self.slice = rest;
Some(&mut first[0])
}
}
fn main() {
let mut data = vec![1, 2, 3, 4];
for x in (IterMut::new(&mut data)) {
*x *= 10;
}
println!("{:?}", data);
}
課題
- 原因の特定:
self.slice = rest;とSome(&mut first[0])のどちらか(または両方)で借用チェッカーがエラーを出します。next(&mut self)のselfが持つ(暗黙の、'aより短い)ライフタイムを仮に'sとして、「first・restが実際にはどのライフタイムを持ってしまっているか」「Item = &'a mut i32が要求するライフタイムとなぜ食い違うか」をライフタイム変数を使って説明してください - 修正:
std::mem::takeを使って、self.slice(&'a mut [i32]という値そのもの)を一度selfの外に取り出してから分割する形に書き換え、コンパイルを通してください(&mut [T]はDefaultを実装しており、空スライス&mut []を返すためmem::takeが使えます) - 検証:
mainの通りにdataの各要素を10倍できることを、#[test]で確認してください。あわせて「IterMutを2回forループに通せない(IntoIteratorではなく直接Iteratorを実装しているため2周目は空になる)」ことも1つのテストで確認してください
🔍 ヒント(段階的開示)
ヒント1 — 方向性
self.slice.split_at_mut(1)という書き方は、self.slice(フィールド、型は&'a mut [i32])に対してメソッド呼び出しのためだけに一時的な再借用(reborrow)を発生させます。この再借用は「メソッド呼び出しの引数として&mut *self.sliceを作る」のとほぼ同じ意味で、その借用の生存期間は&mut self自体の生存期間(今回はnext呼び出し中の短い期間)に縛られます- 「
self.sliceというフィールドの値そのもの(='aを持った&mut [i32]という参照)を借りるのではなく、丸ごと取り出して自分のものにする」という発想の転換が必要です
ヒント2 — アプローチ
std::mem::take(&mut self.slice)は、「self.sliceの現在の値をムーブで取り出し、代わりにDefault::default()(&mut [T]の場合は空スライス)を書き戻す」という操作です。これにより、取り出した値はもうselfを経由した借用ではなく、ローカル変数が所有する、正真正銘'aを持った値になります- ローカル変数として取り出した後にその値に対して
split_at_mutを呼べば、結果のfirst・restは元の'aをそのまま引き継ぎます。あとはrestをself.sliceに書き戻し、firstから作った&mut i32をItem = &'a mut i32として返せます
ヒント3 — コード骨格
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut i32;
fn next(&mut self) -> Option<Self::Item> {
// ① self.slice を "所有権ごと" ローカル変数へ取り出す(self.slice には空スライスが残る)
let slice = std::mem::take(&mut self.slice);
if slice.is_empty() {
// ここで self.slice を戻す必要はない(空のまま = 取り出し済みでちょうど良い)
return None;
}
let (first, rest) = slice.split_at_mut(1);
self.slice = rest; // rest は 'a を持つので代入OK
Some(&mut first[0]) // first も 'a を持つので Item = &'a mut i32 に一致
}
}
✅ 模範解答
// このコードは cargo new day022_itermut && cd day022_itermut の後、
// src/main.rs をこの内容で置き換えて `cargo run` / `cargo test` で検証できます。
struct IterMut<'a> {
slice: &'a mut [i32],
}
impl<'a> IterMut<'a> {
fn new(slice: &'a mut [i32]) -> Self {
IterMut { slice }
}
}
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut i32;
fn next(&mut self) -> Option<Self::Item> {
// self.slice というフィールドの "値" を丸ごと取り出す。
// self.slice には Default::default()(&mut [i32] の場合は空スライス &mut [])が
// 一時的に書き戻される。
let slice = std::mem::take(&mut self.slice);
if slice.is_empty() {
return None;
}
// slice はもう self 経由の借用ではなく、'a をそのまま持つローカル値なので、
// ここで作る first / rest も 'a を引き継ぐ。
let (first, rest) = slice.split_at_mut(1);
self.slice = rest;
Some(&mut first[0])
}
}
fn main() {
let mut data = vec![1, 2, 3, 4];
for x in IterMut::new(&mut data) {
*x *= 10;
}
println!("{:?}", data);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doubles_each_element_via_mutable_iteration() {
let mut data = vec![1, 2, 3, 4];
for x in IterMut::new(&mut data) {
*x *= 10;
}
assert_eq!(data, vec![10, 20, 30, 40]);
}
#[test]
fn iterator_is_exhausted_after_one_pass() {
let mut data = vec![1, 2, 3];
let mut it = IterMut::new(&mut data);
// 1周目: 全要素を消費する
let count_first_pass = it.by_ref().count();
assert_eq!(count_first_pass, 3);
// 2周目: IterMut は IntoIterator ではなく Iterator を直接実装しているため、
// 同じインスタンスをもう一度 next() しても None しか返らない
assert_eq!(it.next(), None);
}
}
▶ 実行結果を見る(cargo run / cargo test・実機で検証済み)
$ cargo run
[10, 20, 30, 40]
$ cargo test
running 2 tests
test tests::doubles_each_element_via_mutable_iteration ... ok
test tests::iterator_is_exhausted_after_one_pass ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
🪜 Step-by-Step 解説
next(&mut self)を呼ぶとき、コンパイラはselfに対して「このメソッド呼び出しの間だけ有効な、名前のない短いライフタイム」を割り当てます。これを'sと呼ぶことにします('sはnextが返るまでの間しか有効ではなく、一般に'aより短い)。元のコードのself.slice.split_at_mut(1)は(&mut *self.slice).split_at_mut(1)と脱糖され、first・restの実際の型はコンパイラの推論上&'s mut [i32]に近いもの('sに縛られたスライス)になります。一方、関数シグネチャが要求しているのはItem = &'a mut i32であり、self.slice: &'a mut [i32]への代入も'aを要求します。つまり「'sを'aとして扱おうとする」ライフタイムの不一致が、コンパイルエラーの正体です。'sはnext呼び出しごとに使い捨てられる短い期間でしかないため、コンパイラはこれを'a(インスタンス全体が生きている限り有効)に格上げすることを許しません。
std::mem::takeで「借用」を「所有権の移動」に変える修正版のポイントは1行目です。
let slice: &'a mut [i32] = std::mem::take(&mut self.slice); — mem::take(&mut self.slice)は、self.sliceフィールドの中身をムーブで取り出し、代わりに<&mut [i32] as Default>::default()(=空スライス&mut [])をそこに書き戻します。この操作自体は&mut self('s)を借りていますが、戻り値として取り出されるsliceは、元々self.sliceが持っていた'aというライフタイムをそのまま保持したローカル変数になります('sに縛られた「再借用」ではなく、値そのものの「移動」だからです)。以降のslice.split_at_mut(1)は'sに縛られていないsliceに対する呼び出しなので、結果のfirst・restも'aを引き継ぎ、self.slice = rest;もSome(&mut first[0])も要求されるライフタイムと一致してコンパイルが通ります。
mem::takeした直後、self.sliceは一瞬&mut [](空スライス)になります。もしnext()の実行途中でpanicが起きても、self.sliceは「空スライス」という常に有効な値であり続けるため、未初期化状態や不正なメモリ状態は発生しません。これは「一時的に値を取り出して加工し、書き戻す」というRustの定石パターン(mem::take / mem::replace / mem::swapの組み合わせ)が安全である理由でもあります。
💡 設計思想・なぜこう書くのか
このバグは「Rustの借用チェッカーが不便」なのではなく、「イテレータが1度に1個の要素だけを外に貸し出す」という契約を、型システムのレベルで強制している結果です。もし仮にこの制約が緩くてItem = &'a mut Tを素朴な再借用で実装できてしまったら、次のようなコードが「コンパイルは通るが実行時に未定義動作」になる余地が生まれます(実際にはRustではこの制約のおかげでそもそも書けません)。
let mut it = IterMut::new(&mut data);
let a = it.next().unwrap(); // 1個目への &mut
let b = it.next().unwrap(); // 2個目への &mut (a はまだ生きている)
// もし a と b が同じ要素を指していたら、2つの &mut が同時に存在してしまう
mem::takeを使った実装は、self.sliceの所有権を一時的に手放し、分割してから、残りだけを戻すという手続きを踏むことで、「一度に外へ出す&mutは常に1個だけ、かつ残りの部分と重ならない」ことを型レベルで機械的に保証します。これは「エイリアシング(同じデータへの複数の可変参照)はコンパイル時に排除する」というRustの中心的な設計哲学が、まさにイテレータの実装という具体的な場面でどう効いてくるかを示す好例です。unsafeで実装されている: 標準ライブラリ自身のslice::IterMut(Vec::iter_mut()が返す型)は、実は今日のような安全なコードではなく、内部で生ポインタ(raw pointer)とunsafeを使って実装されています。理由は、mem::take方式は「毎回1要素ずつ分割し直す」ため理論上のオーバーヘッドがゼロではない場面があり、標準ライブラリはそこをunsafeで直接ポインタ演算することでゼロコストに寄せているためです。「安全なmem::take版」と「unsafeな生ポインタ版」の比較は、Phase 3「unsafe Rustの基礎」で扱う予定です。🛑 コンパイルエラーが出た場合
元のコード(mem::takeを使わない版)をcargo buildすると、実際に次のエラーになります(本稿の検証環境: rustc 1.96.0。行番号はファイル内の位置によって前後しますが、メッセージ本文はバージョンによらずほぼ同じです)。
読み方:
lifetime 'a defined here:impl<'a> Iterator for IterMut<'a>の'aが「構造体が本来持っているべき長いライフタイム」であることを示していますlet's call the lifetime of this reference '1:&mut selfに割り当てられた「このメソッド呼び出し限りの名前のないライフタイム」に、コンパイラが仮の名前'1(本稿の'sに相当)を付けていますassignment requires that '1 must outlive 'a: 「rest('1に縛られている)をself.slice('aを要求するフィールド)へ代入するには、短い'1の方が長い'aより長生きしていないといけない」という、成立しえない要求を突きつけられている、というのがこのエラーの意味です
直し方の方針: 「selfを経由した再借用('1に縛られる)」ではなく、「self.sliceの値そのものをムーブして手元に持ってくる('aを保つ)」形に書き換える、というのが今日学んだmem::takeの役割です。&mut selfの中に長いライフタイムの値が入っている場合、それを「借りる」のではなく「一時的に取り出す」という考え方は、今後カスタムイテレータ以外の場面(ステートマシンの状態遷移、Option<T>::take()を使った所有権の付け替えなど)でも繰り返し登場する重要なパターンです。
🌐 他言語との比較
| 言語 | 「進めた後に前の要素への参照を使い続ける」問題 |
|---|---|
| C++ | std::vector<T>::iteratorは生ポインタに近い実装が多く、要素への参照/イテレータを保持したままpush_back等でコンテナが再確保されるとイテレータ無効化(iterator invalidation)という未定義動作に陥る。コンパイラは検出しない |
| Java | Iteratorで要素を取り出した後にコレクション本体を構造変更すると、次のnext()呼び出しで実行時にConcurrentModificationExceptionが飛ぶ(Fail-Fastの仕組み)。コンパイル時には検出できない |
| Go | スライスのループ変数はループ内で使い捨てるのが基本で、「進めた後も前の要素への参照を保持し続ける」ようなイテレータ抽象自体が言語標準にはあまり存在しない(Go 1.23以降のiter.Seqでも、値のエイリアシング安全性はコンパイラでは保証されない) |
| Rust | 今日のように、「進めた後も前の&mutを使い続けられてしまう」設計自体をコンパイル時に弾く。実行時例外もUBも発生せず、「そもそもそのコードを書けない」状態にする |
C++/Javaでは「イテレータを進めた後に古い参照を使う」バグは、テストで踏み抜くかテストをすり抜けて本番で発覚するかの二択になりがちです。Rustではこのクラスのバグがコンパイルの段階で機械的に潰れるため、レビューやテストのコストをその分だけ他の領域(ロジックの正しさなど)に回せる、というのが実務上の利点です。
🏆 実務での使いどころ
Vec<T>::iter_mut()/slice::iter_mut(): 「全要素を1回ずつ書き換える」処理(例: バッチ処理でのフィールド更新、正規化処理)で標準的に使うHashMap<K, V>::values_mut(): マップの値だけをその場で更新するとき(例: 全ユーザーのポイント残高に一律ボーナスを加算する処理)- 自作のリングバッファ・スロット配列などで「各スロットへの
&mutを1個ずつ、順番に、安全に」貸し出したいカスタムコンテナを書くとき、今日のmem::takeパターンがそのまま使える - ゲームエンジンのECS(Entity Component System)実装で「あるコンポーネント配列の各要素へ
&mutを配りながら並行処理する」ようなクエリイテレータの実装にも、同じ考え方(「値を一時的に取り出して分割し、残りを戻す」)が土台になっている
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
「&mut selfの中の&'a mut Tは、そのまま'aとして外に返せるはず」という思い込み | 所有権付きの値(StringやVec<T>そのもの)を返す感覚のまま、参照フィールドにも同じ直感を当てはめてしまう | フィールドが参照型の場合、&mut self経由でアクセスすると「フィールドの値そのもの」ではなく「フィールドへの再借用」になり、ライフタイムが&mut selfの借用期間に短縮される |
mem::takeの代わりに.clone()やunsafeで無理やり型を合わせようとする | エラーメッセージだけを見て「型を合わせればいい」という表面的な対処に走ってしまう | &mut TはCloneできない(同じデータへの複数の可変参照になってしまうため)。unsafeに頼らず、mem::take/mem::replaceで「一時的に所有権を移す」のが安全な定石 |
「mem::takeした後、self.sliceが空になったままバグる」と心配する | takeの副作用(元の場所にDefault::default()を書き戻す)を「値が消えてしまう」と誤解する | next()の最後(または早期リターン前)に必ず適切な値(rest、または空のまま)をself.sliceに書き戻しているので、呼び出しが終わった時点では一貫した状態に戻っている |
IterMutを2回forに通せると思ってしまう | Vecなどの「所有権を持つコレクション」と「一度きりのイテレータ」を混同する | IterMut<'a>はIteratorを直接実装しており、IntoIteratorではない。1周消費したらself.sliceは空スライスのままなので、2周目は必ず空になる(今日のテストで明示的に確認した通り) |
🚀 次のステップ
- 発展: 今日は
Item = &'a mut i32という固定型で実装しましたが、TをジェネリックにしたIterMut<'a, T>に一般化してみましょう(T: Defaultは不要で、&mut [T]自体が任意のTに対してDefaultを実装している点に注意) - 次回予告: 自己評価で「完全理解」または「おおむね理解」が3回連続で記録された時点で、次のテーマスマートポインタ①(Box<T>)(ヒープ確保・再帰的データ構造)に進みます