背景
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