Code Fix

中級

each で配列を回しながら delete すると要素が飛ばされる

eachで配列を反復しながらdelete_atやdeleteで要素を取り除くと、後続の要素が前に詰まってインデックスがずれ、次に処理されるはずだった要素が読み飛ばされます。

なぜエラーが出ないのか

出力: [4, 3, 5]
(エラーなし)
Rubyは何も報告しません。文法として正しいためです
出力: [4, 3, 5]
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  nums = [2, 4, 6, 3, 8, 5]
 2  nums    .each do |n|
        ^
 3    nums.delete(n) if n.even?
 4  end
 5  puts nums.inspect
出力: [4, 3, 5]

元の配列を直接eachで回しながらdeleteすると、削除によるずれで次の要素が読み飛ばされ、4のような偶数が消えずに残ります。

直し方: nums を nums.dup にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  vals = [4, 2, 8, 1, 6, 3]
 2  vals    .each do |n|
        ^
 3    vals.delete(n) if n.even?
 4  end
 5  puts vals.inspect
出力: [2, 1, 3]

反復中の配列そのものを変更すると、インデックスがずれて意図しない要素が生き残ります。

直し方: vals を vals.dup にします。

この問題を解いてみる →

パターン3

 1  data = [6, 2, 4, 9, 8, 1]
 2  data    .each do |n|
        ^
 3    data.delete(n) if n.even?
 4  end
 5  puts data.inspect
出力: [2, 9, 1]

安全に取り除きたい場合は、複製した配列を反復しながら元の配列を変更するようにします。

直し方: data を data.dup にします。

この問題を解いてみる →

パターン4

 1  items = [8, 4, 2, 5, 6, 7]
 2  items    .each do |n|
        ^
 3    items.delete(n) if n.even?
 4  end
 5  puts items.inspect
出力: [4, 5, 7]

eachは削除によって縮んだ配列を考慮せず、内部のインデックスを機械的に進めるだけです。

直し方: items を items.dup にします。

この問題を解いてみる →

パターン5

 1  queue = [2, 6, 4, 1, 8, 9]
 2  queue    .each do |n|
        ^
 3    queue.delete(n) if n.even?
 4  end
 5  puts queue.inspect
出力: [6, 1, 9]

dupで複製したスナップショットを反復すれば、元の配列をいくら変更してもずれの影響を受けません。

直し方: queue を queue.dup にします。

この問題を解いてみる →

よくある誤解

「削除した分だけ後ろにずれても、eachが自動で調整してくれるはず」という思い込みは誤りです。eachは内部でインデックスを進めているだけなので、削除でずれた要素は無条件に読み飛ばされます。反復中に安全に取り除きたい場合はselectやreject(または元の配列を複製してから回す)を使う必要があります。

実務での勘所

each_with_indexを使っても添字を自分で管理しているわけではないため、この問題は解決しません。安全に「条件に合う要素を取り除く」ためにRubyが用意している専用のメソッドがdelete_if(またはreject!)で、内部で反復と削除を安全に両立させる実装になっています。array.delete_if { |x| 条件 }と書けば、eachとdeleteを手で組み合わせるより短く、かつこの読み飛ばしの問題を気にせずに書けます。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)