LoginSignup
2
3

More than 5 years have passed since last update.

Enumerable#maxの罠

Last updated at Posted at 2017-08-26

配列から最大値を求めたい場合数値であれば

[1, 5, 3, 2].max
=> 5

のようにEnumerable#maxメソッド
を使えば簡単に最大値を求めることができます。

でこんどはブロックを渡して戻り値の最大値を求めようと思って以下のように書くと

["hoge", "foobar", "baz"].max{|s| s.size}
=> "baz"

意図した結果と違い、あれ?なんでや?となります

ドキュメントを見ると

ブロックの評価結果で各要素の大小判定を行い、最大の要素、もしくは最小の n 要素を返します。 引数を指定しない形式では要素が存在しなければ nil を返します。 引数を指定する形式では、空の配列を返します。ブロックの値は、a > b のとき正、 a == b のとき 0、a < b のとき負の整数を、期待しています。該当する要素が複数存在する場合、どの要素を返すかは不定です。

つまりブロックの戻り値の最大値を比較しているのでなくブロックの戻り値を<=>演算子の結果として利用しているということです。

["hoge", "foobar", "baz"].max{|s| p s.size; s.size}
6 # "hoge" < "foobar"
3 # "foobar" < "baz"

なので戻り値がマイナスとなるようにすれば結果は逆になります

["hoge", "foobar", "baz"].max{|s| p -s.size; -s.size}
-6 # "hoge" > "foobar"
-3 # "foobar" > "baz"
=> "hoge"

Enumerable#maxメソッドを使って意図した動作をさせるためにはブロックに引数を2つ渡してブロック内で <=> 演算子で比較する必要があります

["hoge", "foobar", "baz"].max{|a, b| p a.size <=> b.size; a.size <=> b.size}
1  # "hoge" < "foobar"
-1 # "foobar" > "baz" 
=> "foobar"

となり正しい結果が返ってきました。

また、こんな面倒なことをしなくてもEnumerable#max_byメソッド
を使えばブロックの戻り値が最大のものを返す元々maxで期待していた動作をします。
また、maxの際は比較ごとにsizeメソッドが呼ばれるのに対し、max_byでは各要素のsizeメソッドは1度ずつしか呼ばれないため処理効率からみてもこちらの方が良いコードです。
(@scivola さんのコメントより)

["hoge", "foobar", "baz"].max_by{|s| s.size}
=> "foobar"

たまたまmaxが正しい結果を返したりして気づかない場合もあるので、このmax, max_byの挙動は注意した方が良さそうです

その他min, minmax(こんなメソッドあるんですね)についても同様です。リンクを貼っておきます。

Enumerable#min
Enumerable#min_by
Enumerable#minmax
Enumerable#minmax

2
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3