【Rubyのちょっとした罠】eachメソッド内でレシーバの配列をdeleteするときに注意すること

Not image

みなさん、こんにちは。

今回は、Rubyの配列でeachメソッドを使うときに気をつけたいことをまとめていこうと思います。

内容自体は、冷静に考えればなんてことないことですが、プログラムを考えている段階で煮詰まっていたりすると罠にハマったりするので、紹介しようと思います。

eachメソッド内で配列をdeleteするときの注意点

eachメソッドで配列をdeleteできない?

今回対象となる問題は以下のようなことです。

上のように一見レシーバであるsample_arrという配列から、compare_sample_arrという配列と同じ要素を削除するコードです。

つまり、sample3とsample9という要素は削除されていることを期待しているわけです。

しかし実際のところは削除されていない。

なぜだろう?なぜだろう?なぜだろう?なぜだろう?・・・・・・

これで無事にドツボにはまったわけです。

このコードは、エラーが出ていないというところも罠感があります。一見できていると思って次の処理にいって、ずっと後に気づくこともあるかもしれません。

eachメソッド内でdeleteメソッドが実行されない原因

まず、この問題にエラーが返ってくることがなかったため、Ruby自体の仕様なのではないかという類推ができます。

そこで調べてみると、配列を回すeachメソッドとdeleteメソッドの削除の実行のタイミングが早いか遅いかが原因のようです。

eachとdeleteの実行の順番は以下の通りです。(sample1,sample2,sample3の要素に注目します)

  1. sample1要素は、問題なくスルーです。
  2. sample2要素は、if文でtrueなのでdeleteが実行されます。
  3. deleteが実行されると、sample2が削除されます。
  4. sample2が削除されると、sample1とsample3の間が詰められます!
  5. 詰められたのは良しとして、sample2要素が繰り返し処理の中のブロック変数だった。
  6. eachメソッドはsample2が詰められた瞬間sample3をブロック変数にしてしまいます。
  7. そしてeachメソッドに処理が戻り、次の処理ではsample4要素がブロック変数になります。

このような流れでsample3とsample9は削除されずに配列に残ってしまいました。

これがeachメソッド内でdeleteメソッドが実行されない場合を生むのです。

配列を繰り返し処理でdeleteするときはdelete_ifメソッド

こんな時どのようなメソッドを使えばいいかということですが、結論から申し上げると「delete_ifメソッド」を使いましょう。

以下のコードを見てください。

このように今度は本当に無事に処理が期待通りになっています。sample3とsample9要素が削除されています。

delete_ifメソッドというのは、繰り返し処理をし戻り値がtrueの要素を削除するメソッドです。

ここで記事冒頭に立ち返ると、冷静に考えればdelete_ifメソッドを使えば良いことが分かるのですが、一度eachメソッドを使ってdeleteメソッドをブロック内で実行しようという考えになると、煮詰まるばかりです。

先人のいう通り、困ったときは冷静になることが大切ですね。

まとめ

今回はRubyのちょっとした罠を題材にまとめてきましたが、いかがだったでしょうか?

eachメソッドはなんだかんだ使う機会があるメソッドなので、少し頭の片隅にでも置いておくといいかもしれません。

本当は、deleteされた要素をnilとかに置き換えるような処理でも良さそうだなと思ったのですが、Rubyの仕様が「詰める」という選択になっているのは他のところで問題が出てくるからということなのでしょうかね?

まあしかし、「今回のような場合はdelete_ifを使ってね」というRubyの仕様に従うことが、可読性のある美しいRubyコードを生むのだと思います。

今回も読んでいただきありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です