本記事は個人ブログからの焼き直しです。1
tl;dr
配列の複数要素の削除はdelete_ifかreject、-なんかを使いましょう
each内でdeleteした場合に問題が起きる
Rubyで配列があったとします。
この中で、特定の文字列を含む要素を1つだけ削除したいです。
それであればArray.delete('特定の文字列')で済みます。
もし、その特定の文字列というのが複数ある場合はどうしましょう?
複数あるなら、それを配列にでも突っ込んで、イテレートしてdeleteしていけばいいんじゃない?
と考えたのが以下の実装です。
target = ['hoge', 'fuga'] # 元となるデータ
list = ['hoge', 'fuga'] # 削除対象の文字列のリスト
target.each do |str|
target.delete(str) if list.include?(str)
end
p target # ["fuga"]
なんとfugaが残ってしまいました!
よく考えれば、分かることかもしれませんが、イテレートしている最中に要素を削除することで内部ポインタがずれてしまうのです。
ここの例だと配列の0番目はhogeです。
最初のループでstrにはhogeが入り、
if list_include?
にかけられてtrue、target.deleteが実行されます。
その直後にtarget[0]をprintしてみると、fugaが入っています。
target = ['hoge', 'fuga'] # 元となるデータ
list = ['hoge', 'fuga'] # 削除対象の文字列のリスト
target.each_with_index do |str|
p target[0] # 'hoge'
target.delete(str) if list.include?(str)
p target[0] # 'fuga'
end
# 要素数(hoge削除後のfuga1個)とループの数(hogeを削除したときの1回)が一致したので1周しかしない
p target # ['fuga']
ということで、配列の場合はポインタが狂ってしまうので、削除にeach&deleteを使うのはオススメしない、という話です。
こういうこともあるので用意されているのが、delete_if、rejectメソッドということなんですね。
delete_if, rejectなら問題なし
delete_ifやrejectメソッドはこういうことがおきません。
target = ['hoge', 'fuga'] # 元となるデータ
list = ['hoge', 'fuga'] # 削除対象の文字列のリスト
target.delete_if do |str|
list.include?(str)
end
p target
用意されているにはそれなりにワケがありますね。
#-メソッドもあるよ
元記事のほうのコメントでshinoさんという方からご指摘を頂きましたが#-メソッドを使えば差を求めることができ、同様の結果になります。
(推測混じりですが)delete_ifなどと違いEnumeratorを使うわけではなさそうなので要素数が非常に多い環境では顕著に高速に動作するものと思われます。
require 'benchmark'
srand(62438757249)
target = (1..1000000).map{ rand(10000) } # 100万要素の配列を作成
list = (1..10000). map{ rand(100000) } # 削除対処のデータを作成
delete_if_target = target.dup # delete_ifだと元データから削除されてしまうのでdelete_if用にデータをコピー
res = nil # -メソッドの結果用の変数初期化
Benchmark.bm(10) do |r|
r.report('delete_if') do
delete_if_target.delete_if {|v| list.include?(v) }
end
r.report('- method') do
res = target - list
end
end
# user system total real
# delete_if 365.790000 2.400000 368.190000 (379.281810)
# - method 0.110000 0.010000 0.120000 ( 0.118205)
p delete_if_target == res # true 結果が同値であることを確認
-の方が速攻で終わってますね。
まとめ
データ量ややりたいことに応じてメソッドを使い分けましょう。