背景
Rails において、 nil? , empty?, blank?, present? はよく使われる便利なメソッド群です。
しかし、これらの便利メソッドには、分かりづらい落とし穴が潜んでいます。例えば、String#blank? に対する以下のコードは、一見 true を返しそうですが、実は間違いなのです。
string.blank? == string.nil? || string.empty?
この記事においては、便利メソッドの使いわけを、ついつい引っかかりがちな落とし穴と一緒に解説し、より良いRailsのコードを書くためのBetter Practiceを紹介したいと思います。
TL; DR
String において、 nilと空白文字を同様に評価したいときのみ、blank? や present? を使うのが Better Practice
より細かくは
- Rubyで定義されているのが、
nil?,empty?, Railsの ActiveSupport で定義されているのが、blank?,present? -
nil?は レシーバがnilであるとき、trueを返す。nilに対して呼べる数少ないメソッド。 -
empty?はレシーバが**「空」**であるとき、trueを返す。 -
blank?はレシーバが**「空白」**であるとき、trueを返す。- 空白とは、空であることに加えて、
nil,false, 空白文字で構成されているということを含む
- 空白とは、空であることに加えて、
-
present?は常に!blank?のことである。
表にまとめると以下のようになります。
| レシーバ | nil? | empty? | blank? | present? |
|---|---|---|---|---|
| nil | true | NoMethodError | true | false |
| true | false | NoMethodError | false | true |
| false | false | NoMethodError | true | false |
| [] | false | true | true | false |
| {} | false | true | true | false |
| "" | false | true | true | false |
| " " | false | false | true | false |
各メソッドの詳解
nil?
nil?はレシーバが 「nil」 であれば真を返すメソッドです。
nil.nil? #=> true
"".nil? #=> false
[].nil? #=> false
{}.nil? #=> false
https://docs.ruby-lang.org/ja/2.5.0/method/Object/i/nil=3f.html
https://docs.ruby-lang.org/ja/2.5.0/method/NilClass/i/nil=3f.html
empty?
empty?はレシーバが**「空」**であれば真を返すメソッドです。
例えば、ArrayやHashであれば、要素数が0のとき、Stringであれば、長さが0の時に真を返します。
空白文字であっても、長さが0でなければ偽を返します。また、TrueClass や FalseClassにはこのメソッドは実装されていません。
nil.empty? #=> NoMethodError
true.empty? #=> NoMethodError
false.empty? #=> NoMethodError
[].empty? #=> true
{}.empty? # => true
"".empty? #=> true
" ".empty? #=> false
https://docs.ruby-lang.org/ja/2.5.0/method/Hash/i/empty=3f.html
https://docs.ruby-lang.org/ja/2.5.0/method/Array/i/empty=3f.html
https://docs.ruby-lang.org/ja/2.5.0/method/String/i/empty=3f.html
blank?
blank? メソッドは**「空白」**であるか否かを返します。
より正確な実装は、nilやfalse などの偽と判定されるオブジェクト、[:space:]の空白文字だけで構成されるオブジェクト、その他 empty? なオブジェクトに対して真を返します。
ドキュメントには明示されていませんが、Stringクラスに対しては、内部的には正規表現によるマッチが行われています。そのため、invalidなバイト列があれば、ArgumentErrorが発生することには注意してください。
nil.blank? #=> true
true.blank? #=> false
false.blank? #=> true
[].blank? #=> true
{}.blank? # => true
"".blank? #=> true
" ".blank? #=> true
"\xFF".blank? #=> ArgumentError: invalid byte sequence in UTF-8
present?
present? は常に !blank? になっています。
実践編 (Better Practice)
ここからは筆者の主観に基づいた Better Practice を紹介します。
empty? と blank? の使い分けについて
上述したように、「空」を表現したい場合には、 empty?, 「空白」を表現したい場合には、 blank? を使うことを、コードの可読性や不要なバグを防ぐためにおすすめします。
より具体的には、 String クラスを期待しており、なおかつ nil と 空白文字を同一に扱いたいという場合にのみ blank? を利用し、それ以外の場合は empty? を使うことをおすすめします。
String クラス以外について、 nil チェックの省略を行いたくなった場合には、以下の対策が有効だと思われます。
Safe Navigation Operator の利用
一般的なオブジェクトに対して、 blank? や present? のように、 nil チェックを省略したい場合は Ruby 2.3.0 で導入された Safe Navigation Operator を利用することがおすすめです。
TrueClass, FalseClass
これらについては、true や false を期待している場所に nil が入らないようにコードを改善するのが良いです。なぜなら、真偽値を評価する変数に対して、nilが入った場合には、それは偽として評価されてしまいます。しかし、nilというオブジェクトからは、それが真を表現したいのか、偽を表現したいのかが分からなくなるためです。
if saved? # ここが `nil` の場合は、`save!`されないが、本当に大丈夫なのか?
load
else
save!
end
Array, Hash
ArrayやHashを特定の「集合」を示す為に利用している場合は、 blank? を使って nil チェックを省略するのではなく、nilが代入されるようなコードを、空のオブジェクト ([] や {})が代入されるように修正することが望ましいです。
これは、nilではなく、空のオブジェクトを利用したほうが、「空集合」を適切に表現できる為です。また、呼び出し側でも、空の状態を区別せずに処理できるので、コードの複雑度が削減されます。
一方、複数のattributeを持つ単体の概念をHashで表現している場合は、存在しないことをnilを使って表現するほうがしっくりくる場合も多いです。
# 空集合がnilによって表現されている場合
if people
lucky_people = people.select(&:lucky?)
else
lucky_people = []
end
# 空集合が、空のオブジェクトによって表現されている場合
lucky_people = people.select(&:lucky?)
# 複数のattributeを持つ単体の概念をHashで表現している場合
if prize_data
prize_name = prize_data[:name]
else
prize_name = nil
end
[落とし穴] Rubocop の Rails/Blank
blank? は empty? とは大きく異なるということでしたが、2018年9月現在のRubocopにおいては、 Rails/Blank という cop における NilOrEmpty: true (default) では blank? への間違った変換が行われてしまいます。
# Converts usages of `nil? || empty?` to `blank?`
# bad
foo.nil? || foo.empty?
foo == nil || foo.empty?
# good (実はgoodではない!)
foo.blank?
この挙動が問題になるようであれば、Rubocop YAMLの設定から Rails/Blank を disable にしてしまいましょう。
[落とし穴] ActiveRecord の validation
ActiveRecord の validationにおいて、 presence: true や、その逆である absence: true というものが存在します。これも、Better Practiceの冒頭で述べたように、 String に対して、 nil と空白文字を同様に扱いたい場合にのみ使用するのが良いと思われます。
また、 allow_blank: true についても、 空白を許容する場所以外では利用せず、 allow_nil: true を積極的に使っていくことをおすすめします。
# Stringであり、空白も許容できない場合
validates :good_description, presence: true
# Stringであり、空白である必要がある場合
validates :bad_description, absence: true
# Numericであり、nilが許容される場合
validates :score, numericality: { only_integer: true }, allow_nil: true
# true / false の場合 (presence, absence, allow_blankでは正常動作しない)
validates :activated, inclusion: { in: [true, false] }
https://guides.rubyonrails.org/v5.2/active_record_validations.html#presence
https://guides.rubyonrails.org/v5.2/active_record_validations.html#absence
https://guides.rubyonrails.org/v5.2/active_record_validations.html#allow-blank
参考
- Ruby 2.5.0 リファレンスマニュアル
- Ruby on Rails Guides
- Documentation for rubocop
- nil? empty? blank? present? の使い分け | Qiita