読者です 読者をやめる 読者になる 読者になる

【Ruby】配列の複数要素の削除はdelete_ifかrejectを使おう

zomです。 < 2013/04/10 タイトル修正しました > Rubyの実装で「よく考えればそうだよね」ということがあります。 最近あったところで、掲題のことが言えます。 経緯とかをちょっとメモがてら書いておきます。

Rubyで配列があったとします。この中で、特定の文字列を含む要素を1つだけ削除したいです。 それであればArray.delete('特定の文字列')で済みます。 ただし、その特定の文字列というのが複数ある場合はどうしましょう? 複数あるなら、それを配列にでも突っ込んで、イテレートしてdeleteしていけばいいじゃない。 と考えたのが以下の実装です。

target = ['asparagus', 'bean', 'carrot'] # 元となるデータ
list = ['asparagus', 'bean'] # 削除対象の文字列のリスト
target.each do |str|
 list.include?(str) and target.delete(str)
end
p target # ["bean", "carrot"]

なんとbean(豆)が残ってしまっています!(あえて大袈裟に書いてます笑) よく考えれば、分かることかもしれませんが、イテレートしている最中に要素を削除することで内部ポインタがずれてしまうのです。 ここの例だと配列の0番目はasuparagusです。最初のループでstrにはasuparagusが入り、 list_include?にかけられてtrue、target.deleteが実行されます。 その直後にtarget[0]をprintしてみると、beanが入っています。

target = ['asparagus', 'bean'] # 元となるデータ(邪魔なのでcarrot消しました)
list = ['asparagus', 'bean'] # 削除対象の文字列のリスト
target.each_with_index do |str|
  p target[0] # 'asuparagus'
  list.include?(str) and target.delete(str)
  p target[0] # 'bean'
end
# 要素数(asuparagus削除後のbean1個)とループの数(asuparagusを削除したときの1回)が一致したので1周しかしない
p target # ['bean']

ということで、配列の場合はポインタが狂ってしまうので、削除にeach&deleteを使うのはオススメしない!という話です。 こういうこともあるので用意されているのが、delete_if、rejectメソッドということなんですね。 delete_ifやrejectメソッドはこういうことがおきません。 用意されているにはそれなりにワケがあるって感じですねー。