【Rails】「nilじゃない」と「present?」は同じ? 〜わかりにくいコードに立ち向かう〜


こんにちは
業務でリファクタをしていて、タイトルの件少し気になったのでまとめてみます

少しややこしいですが、頭の体操だと思ってお読みください
(混乱しないように、わかりやすくまとめたつもりではあります。一応。。)

否定表現ってどうしてわかりにくいんでしょうね。ただ意味が逆になるだけなのに...

※ 2021/8/29に修正しています(公式ドキュメントの参照先)

本題

タイトルの通りnilじゃないという条件はpresent?と代替可能か?というテーマです

if !hoge.nil? # => hoge.present?でもいいのでは?
  # ...
end

上記の例だと、「nilの場合に早期リターンすればいいだけじゃん」で片付くかもしれません(それかunless

ですが、条件が複雑になってくるとpresent?にしたい欲が込み上げてきて吐きそうになります🤢
(以下は吐きたくなる例)

if !hoge.nil? && fuga.present? # .。oO hoge.present?にしちゃだめ・・?
  # ...
elsif fuga.status == 'canceled' && piyo.status == 'active'
  # ...
else
  # ...
end

※ ここで「status_canceled?のメソッドを用意しておくべきだろ」みたいなコメントはポケットの中にでもしまっておいてください(本記事の趣旨とは関係ない話なので)

TL;DR

以下の条件を満たすhogeが想定される場合は、置き換えてはいけません

!hoge.nil?
# => true

hoge.present?
# => false

💡つまり、レシーバが以下のオブジェクトとなりうる場合は置き換えできません

  • ''(空文字)
  • ' '(スペースのみの文字列)
  • {}(空のハッシュ)
  • [](空の配列)
  • false

それか、これらのオブジェクトが来ても無視してOKという仕様であれば置き換えても良さそうです

以下「なぜそうなる?」という話が続きます

調査

こういう調査は公式のドキュメントを読むに限ります
もし間違っていても「ドキュメントが悪い」ということで刑が軽くなるからです
(内部のソースコードまでは読んでないので、ガチ勢の方がいらっしゃいましたら優しくご指摘ください)

nil?について

まずはnil?というメソッドについて、調べてみます

レシーバが nil であれば真を返します。

まあ、当たり前ですがnilだけtrueとなります(nil以外はfalse

present?について

続いてpresent?について

An object is present if it's not blank.

ではblank?って何?・・↓

An object is blank if it's false, empty, or a whitespace string. For example, nil, '', ' ', [], {}, and false are all blank.

blank?はいろんなクラス(StringとかActiveRecord::RelationとかNilとか..)に対して定義されています

falseempty空白の文字列はblankという判定になります

ではempty?って何だろう?・・↓
empty?はRuby標準のメソッドです

RailsではActionController::ParametersActiveRecord::Relationなどにも用意されています(他のクラスにもあるので詳しくはドキュメント参照)

例えばActionController::Parametersであればキー/バリューがない場合だったり、ActiveRecord::Relationであればレコードがない場合にはemptyの判定になります
(直感的にはそうですね)

厳密な表現ではないですが、直感的にはpresent?trueを返すオブジェクトは、[空の文字列, スペースのみ文字列, 空のハッシュ, 空の配列, nil, false]の補集合と考えて良さそうです

まあつまり[空の文字列, スペースのみ文字列, 空のハッシュ, 空の配列, nil, false]に対してはfalseを返すってことです

以下、公式ではないですが日本語での調査です

https://railsdoc.com/page/empty

!blank? を実行するメソッド。if 変数.present?とunless 変数.blank?は同じ意味 nil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュのときにfalseを返します Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください

ではblank?って何?・・↓

nil? + empty? のようなメソッド。nilまたは空のオブジェクトをチェック nil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュのときにfalseを返します Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください

ではempty?って何?・・↓

Rubyの標準メソッド。空の文字列や空の配列の場合にtrueを返す。nilに対して呼び出すとNoMethodErrorが発生

検証

present?はRailsでのみ使えるメソッドなので、Railsのコンソールで検証してみます

''.present?
# => false

' '.present?
# => false

{}.present?
# => false

[].present?
# => false

nil.present?
# => false

true.present?
# => true

false.present?
# => false

若干くどいですが、念の為nil?についても試してみます
当然nil?nil以外に対してはfalseです

''.nil?
# => false

' '.nil?
# => false

{}.nil?
# => false

[].nil?
# => false

nil.nil?
# => true

true.nil?
# => false

false.nil?
# => false

当たり前ですが、ドキュメントのとおりの結果となりました

ここでnil?に対してもpresent?に対してもfalseを返しているオブジェクトがあるので、nilじゃないという判定present?と置き換えてはいけないことになります

※ 当然ですが、文脈的に置き換えても問題ないという仕様であれば置き換え可能です(上記のケースをよく考える必要があるので少し疲れます)

まとめ

まとめると、当然のようですが、nilじゃないpresent?は別物になります
置き換えたいときは例外ケースがないか十分に検討してからのほうが良さそうですね

今回の裏テーマとしては、「最初から読みやすいコードを書くように心がけよう」です
読みにくいコードは、これまで問題を起こさず生き延びてきたという謎の実績を武器に私たちの前に立ちはだかるのです😈
そして貴重な時間を半永久的に奪っていきますorz

最初から読みやすいコードを書いてくれる人には「ありがとう」と伝えなくてはなりませんね❤️

1bitでもためになったと感じたら、あなたがすべきことはLGTMをポチることだけです
(他の人にも読んでもらいやすくなりますので)

ではでは良いプログラミングライフを〜