Rails における nil?, empty?, blank?, present? の使い分けとBetter Practice

# 背景

Rails において、 `nil?` , `empty?`, `blank?`, `present?` はよく使われる便利なメソッド群です。

しかし、これらの便利メソッドには、分かりづらい落とし穴が潜んでいます。例えば、`String#blank?` に対する以下のコードは、一見 `true` を返しそうですが、実は間違いなのです。

```ruby
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」** であれば真を返すメソッドです。

```ruby
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`にはこのメソッドは実装されていません。

```ruby
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`が発生することには注意してください。

```ruby
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
```

https://railsguides.jp/active_support_core_extensions.html#blank-questionmark%E3%81%A8present-questionmark

## present?

`present?` は常に `!blank?` になっています。

https://railsguides.jp/active_support_core_extensions.html#blank-questionmark%E3%81%A8present-questionmark

# 実践編 (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 を利用することがおすすめです。

https://docs.ruby-lang.org/ja/latest/doc/news=2f2_3_0.html

### TrueClass, FalseClass

これらについては、`true` や `false` を期待している場所に `nil` が入らないようにコードを改善するのが良いです。なぜなら、真偽値を評価する変数に対して、`nil`が入った場合には、それは偽として評価されてしまいます。しかし、`nil`というオブジェクトからは、それが真を表現したいのか、偽を表現したいのかが分からなくなるためです。

```ruby
if saved? # ここが `nil` の場合は、`save!`されないが、本当に大丈夫なのか？
  load
else
  save!
end
```

### Array, Hash

ArrayやHashを特定の「集合」を示す為に利用している場合は、 `blank?` を使って `nil` チェックを省略するのではなく、`nil`が代入されるようなコードを、空のオブジェクト (`[]` や `{}`)が代入されるように修正することが望ましいです。

これは、`nil`ではなく、空のオブジェクトを利用したほうが、「空集合」を適切に表現できる為です。また、呼び出し側でも、空の状態を区別せずに処理できるので、コードの複雑度が削減されます。

一方、複数のattributeを持つ単体の概念をHashで表現している場合は、存在しないことを`nil`を使って表現するほうがしっくりくる場合も多いです。

```ruby
# 空集合がnilによって表現されている場合
if people
  lucky_people = people.select(&:lucky?)
else
  lucky_people = []
end

# 空集合が、空のオブジェクトによって表現されている場合
lucky_people = people.select(&:lucky?)
```

```ruby
# 複数の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?` への間違った変換が行われてしまいます。


```ruby
# Converts usages of `nil? || empty?` to `blank?`

# bad
foo.nil? || foo.empty?
foo == nil || foo.empty?

# good (実はgoodではない！)
foo.blank?
```

https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Rails/Blank

この挙動が問題になるようであれば、Rubocop YAMLの設定から `Rails/Blank` を disable にしてしまいましょう。

## [落とし穴] ActiveRecord の validation

ActiveRecord の validationにおいて、 `presence: true` や、その逆である `absence: true` というものが存在します。これも、Better Practiceの冒頭で述べたように、 `String` に対して、 `nil` と空白文字を同様に扱いたい場合にのみ使用するのが良いと思われます。

また、 `allow_blank: true` についても、 空白を許容する場所以外では利用せず、 `allow_nil: true` を積極的に使っていくことをおすすめします。

```ruby
# 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 リファレンスマニュアル
  * https://docs.ruby-lang.org/ja/2.5.0
* Ruby on Rails Guides
  * https://guides.rubyonrails.org/v5.2/
* Documentation for rubocop
  * https://www.rubydoc.info/gems/rubocop/index
* nil? empty? blank? present? の使い分け | Qiita
  * https://qiita.com/somewhatgood@github/items/b74107480ee3821784e6