Ruby の破壊的メソッド( uniq! など)が nil を返す場合について
破壊的メソッドとは
レシーバ自身を変更するメソッドのこと。下記コードではshuffle!
が破壊的メソッドで、レシーバ自身の中身をシャッフルしています。そのため、配列のobject_id
はシャッフル前と同じです。
一方、shuffle
は非破壊的メソッドのため、新たなobject_id
が生成され、そこにシャッフルされた配列が格納されています。元の配列の中身は、シャッフル前と変わっていません。
array = ["A", "B", "C", "D"] array.object_id #=> 70124212665900 array.shuffle! array #=> ["C", "A", "D", "B"] array.object_id #=> 70124212665900 new_array = array.shuffle new_array #=> ["B", "A", "C", "D"] new_array.object_id #=> 70124212665360 array #=> ["C", "A", "D", "B"] array.object_id #=> 70124212665900
破壊的メソッドの戻り値について
破壊的メソッドは、レシーバ自身の変更を主な目的として使われますが、非破壊的メソッド同様にちゃんと戻り値が返ってきます。そのため、下記のようにメソッドチェーンをして、簡潔にコードを書くこともできます。
array = ["B", "D", "A", "C"] array.sort!.reverse! array #=> ["D", "C", "B", "A"]
破壊的メソッドが nil を返す場合について
ただし、破壊的メソッドが nil を返す場合には要注意です。
array = ["A", "B", "C", "D"] array.uniq! #=> nil array #=> ["A", "B", "C", "D"] array.uniq #=> ["A", "B", "C", "D"]
uniq!
は重複する要素を削除する破壊的メソッドですが、重複する要素が存在しなかったため、戻り値としてnil
が返ってきました。(戻り値がnil
なだけで、配列の中身がnil
になったわけではありません)
一方、非破壊的メソッドのuniq
は、別のobject_id
を生成し、元の配列と同じ要素の配列を返しています。
これは一見、破壊的メソッドの戻り値が不親切なようにも見えます。しかし、下記のようにレシーバが変更されたかどうかで処理を分けたい場合には便利です。
array = ["A", "B", "C", "D"] if array.uniq! array else "arrayは変更されませんでした" end #=> "arrayは変更されませんでした" array_2 = ["A", "B", "B", "C"] if array_2.uniq! array_2 else "array_2は変更されませんでした" end #=> ["A", "B", "C"]
nil を返すメソッドと、そうでないメソッドについて
このように、レシーバが変更されなかった場合にnil
を返す破壊的メソッドは多くあります。
(例:select!、comact!、flatten!、uniq!、reject!など)
一方、レシーバが変更されることを前提としていて、必ずレシーバ自身を返す破壊的メソッドもあります。
(例:shuffle!、sort!、sort_by!、reverse!、rotate!、delete_ifなど)
sort!
したけど既にソート済みのために順番が変わらなかった場合や、shuffle!
でたまたま元の並びと同じ順番になった場合も、レシーバ自身が返ってきます。
面白いのは、reject!
とdelete_if
の違いです。いずれもレシーバに対する変更の挙動は同じですが、レシーバが変更されなかった際の戻り値が異なります。(reject!
はnil
、delete_if
はレシーバ自身)
とはいえ、どのメソッドの戻り値が nil
になる可能性があるのかは混乱しやすいため、戻り値が必要な時に気を付けるようにすれば良いと思われます。
戻り値が nil であることに気付かないとこうなる
目的:配列の中で、2番目に大きな数字の「種類」を出力したい。(例えば [100, 50, 10, 100] の時は 50 を出力したい)
重複した数字があるかもしれないから、uniq!
で重複を排除してから降順に並べ替えて、2番目に大きな数字を出力するだけだな、簡単〜!
↓
失敗例
numbers = [10, 50, 1, 100] numbers.uniq!.sort! { |a, b| b <=> a } puts numbers[1] #=> 2:in `<main>': undefined method `sort!' for nil:NilClass (NoMethodError)
😇😇😇
上記の例では、配列の中に重複した要素が存在しておらず、uniq!
がnil
を返すためにエラーとなりました。
解決策
numbers = [10, 50, 1, 100] numbers.sort! { |a, b| b <=> a }.uniq! puts numbers[1] #=> 50
sort!
は必ずレシーバ自身を返してくれるので、この順番なら正常に動きます。ただしこれだと重複する要素がたくさんある場合、実行に時間がかかってしまうため、
numbers = [10, 50, 1, 100] numbers.uniq! numbers.sort! { |a, b| b <=> a } puts numbers[1] #=> 50
無理にメソッドチェーンをせずに、こちらのほうがいいかもしれません。
参考
B - 心配性な富豪、ファミリーレストランに行く。 - AtCoder
上記の問題で、何故かいくつかのケースでWA(誤答)が出たためにuniq!
の戻り値について調べていたところ、破壊的メソッドにもいろいろな挙動があることが分かりました。
rubyの破壊的メソッドと非破壊的メソッドのパフォーマンス比較 - Hack Your Design!
なお非破壊的メソッドの場合は、毎回object_id
を新たに生成するため実行速度が落ちる点に注意が必要です。