要の純然たる日記(旧館)

今は http://kanamef.sblo.jp に書いてます

Rubyでイテレーションの内部から要素を消してはいけないという常識

Rubyではイテレーションの内部から要素を消してはいかんのですよ。常識か。
たとえばあるクラスの1から10の値を持つインスタンスのうち、5未満の値を持つインスタンスだけ配列から消すことを考えると、

class C
  @x = 0
  def initialize(x)
    @x = x
  end
  def x
    @x
  end
end

a = []
1.upto(10) do |i| 
  a << C.new(i) 
end

a.each do |e|
  a.delete(e) if e.x < 5
end

a.each do |e|
  print "#{e.x}\n"
end

これだとうまくいかなくて

$ ruby hoge.rb
2
4
5
6
7
8
9
10

のように、5未満なのに残っている要素がある。Array#deleteすると、イテレータが進むときには単純に次の要素に移動するのではなくてインデクスが1つ増加するだけ。たとえば

a = [a0, a1, a2]

が入っているとしよう。このとき、a[0] = a0 を消せば、

a = [a1, a2]

になって、次はa1が処理対象になるかと思ってしまうのだけど、実際にはインデクスが0から1に増えるので、次の処理対象は新しいa[1] = a2になる。古いa[1] = 新しいa[0] = a1 は処理対象にならない。ArrayはあくまでもArrayで、イテレーションではeachだろうとeach_indexだろうと、インデクスが1つ増えるだけというのに違いはない。
上のようなことをやろうと思ったら、

a.delete_if do |e|
  if e.x < 5 then
    true
  end
end

と書かないといけない。消すときの副作用はif文の中に書けばOK。
他には、reverse_indexを使えばイテレーションの内部から要素を消せる。要素を消したときに、その要素以降のインデクスが狂うことが問題なのだから、インデクスを最後から回していけば大丈夫という理屈。美しくないけど。