ごぐたんのブログ

ごぐたんのブログです。

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!nildelete_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を新たに生成するため実行速度が落ちる点に注意が必要です。