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
hogeがnilの時は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
hogeがnilでもnilではなくても、hoge.to_iの結果が返っている。
解説
まずは復習。
hoge&.nil?
これは「nil」を返す。 元々、「&.」はhoge側すなわちレシーバー側がnilだった場合に、レシーバから呼び出さすように書かれているメソッドを呼び出さずに「nil」を返す書式である。*1
そのため、if文や三項演算子で条件として利用すると、else側の処理を実行することになる。
勘違いの要因
irb> nil.nil? => true irb> 10.nil? => false
このように、nil.nil?はtureを、10.nil?はfalseを返す。 そして、hoge&nil?では、hogeには「nil」が入っているので、nil.nil?が動作すると勘違いしてしまった。
これ、たとえばhoge.id
とhoge&.id
とかだと早く気がつけた気がする。
また、そもそも&をつけた要因がrubocopからの指摘事項だったというのも大きい。
hoge.id
からhoge&.id
へ修正しrろとrubocopに指摘されて修正っしたのだが、そのあとでもういちどRubocopを走らせても指摘なし。
そのため同じ分岐処理になるという点で勘違いをしてしまった。
この部分で、しっかりとも戻り値(fooメソッドの戻り値がnilかそれ以外か)に対してのテストコードを書いていれば、もっと早くに気がつけたのだろうと思う。
どう直す?
irb* def foo(hoge) irb* hoge&.to_i irb> end => :foo
すると、こうなる
irb> foo(10) => 10 irb> foo(nil) => nil
もとの結果と同じ。