みずりゅの自由帳

主に参加したイベントやソフトウェア技術/開発について記録しています

hoge.nil?とhoge&nil?の結果を勘違い

Rubyでコードを書いていて、「hoge.nil?」と「hoge&nil?」の結果を勘違いしていたために半日悩んだ。

気がついたら、当たり前じゃん、となった。 前提条件で惑わされていて、こんなバグ埋め込んでちょっと悔しかった。

自戒の意味で記録しておく。

勘違いした内容

以下の2つコードで、結果が同じになる、と思い込んでしまっていた。

  hoge.nil? ? nil : hoge.to_i
  hoge&.nil? ? nil : hoge.to_i

ぱっと見で同じになるように思えるけど、ならない。

理由がわからない人は、読み進めてもらうとよい。

確認

それぞれ、実行してみると以下のようになる。

まずは&なし。

irb* def foo(hoge)
irb*   hoge.nil? ? nil : hoge.to_i
irb> end
=> :foo

これはこうなる。

irb> foo(10)
=> 10
irb> foo(nil)
=> nil

hogenilの時はnilが、nilではない時はhoge.to_iの結果が返っている。

では、もう1つの方。

次は&ありの場合。

irb* def foo(hoge)
irb*   hoge&.nil? ? nil : hoge.to_i
irb> end
=> :foo
irb> foo(10)
=> 10
irb> foo(nil)
=> 0

hogenilでもnilではなくても、hoge.to_iの結果が返っている。

解説

まずは復習。

hoge&.nil?

これはnil」を返す。 元々、「&.」はhoge側すなわちレシーバー側がnilだった場合に、レシーバから呼び出さすように書かれているメソッドを呼び出さずに「nil」を返す書式である。*1

そして、Rubyにて、条件判定時のnilはfalse扱い。

そのため、if文や三項演算子で条件として利用すると、else側の処理を実行することになる。

勘違いの要因

irb> nil.nil?
=> true
irb> 10.nil?
=> false

このように、nil.nil?はtureを、10.nil?はfalseを返す。 そして、hoge&nil?では、hogeには「nil」が入っているので、nil.nil?が動作すると勘違いしてしまった。

これ、たとえばhoge.idhoge&.idとかだと早く気がつけた気がする。

また、そもそも&をつけた要因がrubocopからの指摘事項だったというのも大きい。

hoge.idからhoge&.idへ修正しrろとrubocopに指摘されて修正っしたのだが、そのあとでもういちどRubocopを走らせても指摘なし。 そのため同じ分岐処理になるという点で勘違いをしてしまった。

この部分で、しっかりとも戻り値(fooメソッドの戻り値がnilかそれ以外か)に対してのテストコードを書いていれば、もっと早くに気がつけたのだろうと思う。

どう直す?

nilの時にnilを返したいのであれば、以下でよい。

irb* def foo(hoge)
irb*   hoge&.to_i
irb> end
=> :foo

すると、こうなる

irb> foo(10)
=> 10
irb> foo(nil)
=> nil

もとの結果と同じ。

*1:なお、「nil?」はNilClassにも用意されているメソッドなのでそのまま処理されているが、これがhoge.idとかの場合には、「NoMethodError」が出ていたはずである。