はじめに:Rubyの&.演算子やtryメソッドについて
Rubyには&.
という演算子があります。
この演算子を使うと、メソッドのレシーバがnil
(他の言語でいうところのnull
)であっても、メソッド呼び出し時にエラーが起きません。(nil
が返る)
# ユーザが見つかる場合
user = find_user('Alice')
user&.name #=> "Alice"
# ユーザが見つからない場合(userがnilの場合)
user = find_user('Foo')
user&.name #=> nil
同じようなメソッドに、Railsのtry
メソッドがあります。
# ユーザが見つかる場合
user = find_user('Alice')
user.try(:name) #=> "Alice"
# ユーザが見つからない場合(userがnilの場合)
user = find_user('Foo')
user.try(:name) #=> nil
&.演算子やtryメソッドを多用しすぎることの問題
&.
やtry
を使うと、nil
かどうかの条件分岐を書かなくて済むので、非常に便利です。
ですが、こうしたテクニックを多用しすぎると、奇妙なコードを作り出す原因になります。
たとえば次のようなコードです。
def find_friend_message(name)
friend = self.user&.friends&.find_by(name: name)
if friend&.message.blank?
'no message'
else
friend&.message
end
end
上のコードでは&.
が4箇所も出てきます。
とりあえず、一番最後に出てくるfriend&.message
の&.
は不要です。
なぜなら上の条件分岐(if friend&.message.blank?
)で、friend
がnil
でないことが保証されているからです(にもかかわらず、&.
をおまじないのように付けまくる人はよく見かけます)。
しかし、それを無視したとしても、まだ&.
が3箇所も出てきます。
&.
だとさらっと書けてしまうので、問題が見えづらいですが、上のコードは次のようなコードを書いていることと同じです。
def find_friend_message(name)
unless self.user.nil?
unless self.user.friends.nil?
friend = self.user.friends.find_by(name: name)
unless friend.nil?
unless friend.message.blank?
return friend.message
end
end
end
end
'no message'
end
こんなロジック見せられたら、普通は「うえっ」ってなりますよね。
でも&.
によって消臭されてるだけで、実際やってることはこれと同じなわけです。
オブジェクトがnil
/null
になる可能性がある言語は、大なり小なり、「nil
/null
に対してメソッドを呼びだして爆死💥☠️」のリスクがあります。
しかし、そのリスクを過剰に恐れてしまうと、上のコードのように「これはnil
かもしれない、これもnil
かもしれない、全部nil
かもしれない・・・!!!」と思って、&.
やtry
を連発してしまうことになります。
いわば「全部nil(null)かもしれない症候群」です。
この問題の処方箋
「全部nil(null)かもしれない症候群」を治すためには、次のようなアプローチを取るのが良いでしょう。
モデルやメソッドの設計をきちんと確認する
まず、&.
やtry
を使う前に「これはnil
かもしれない」ではなく、「ここはnil
が来る可能性が十分ある」なのか、「nil
は通常あり得ない」なのかをハッキリさせる必要があります。
そのためにシステムのデータ構造(DB設計やDB制約、バリデーション処理等)や、呼びだしたメソッドの戻り値をきちんと確認して、nil
が来るのか、来ないのかをハッキリさせましょう。
エラーを恐れない
「nil
は通常あり得ない」のであれば、やみくもに&.
やtry
を付けるのはやめましょう。
通常あり得ないのに、nil
がやってきたのであれば、それは異常事態です。
実行時エラーを発生させてプログラムを停止させ(&.
を使わなければエラーが発生します)、nil
がやってきた原因を調査すべきです。
下手にnil
を許容すると、そこで問題が起きなくても、他の場所で別の問題を引き起こす恐れがあります。
また、コードを読んだ人間も「ここ、&.
を使ってるけど、nil
のケースがあるのかな・・・??」と首をかしげることになります。
つまり、一種の可読性の低下につながります。
ガード条件をうまく使う
「nil
がありえる、なおかつ、nil
でないときだけ処理したい」という場合は、&.
を連発するよりも、ガード条件を使った方が可読性がよくなることがあります(まあ条件分岐が多い問題は解決してないのですが)。
def find_friend_message(name)
no_msg = 'no message'
return no_msg if self.user.nil?
friend = self.user.friends.find_by(name: name)
return no_msg if friend.nil?
friend.message.blank? ? no_msg : friend.message
end
そもそもの実装を見直す
もし、&.
を連発しているのであれば、そもそも何か根本的な間違いをしている可能性もあります。
クラス設計やメソッドの責務を見直すことで、きれいな実装に直せるかもしれません。
まとめ
&.
演算子やtry
メソッドは便利ですが、多用しすぎると逆にデメリットが出てきます。
「全部&.
を付けておけば、エラーが起きないからあんしーん!」ではなく、
- 原則、
&.
は付けない。本当にnil
を考慮すべきタイミングに限って使用する -
&.
を連発してしまったら、自分の設計や実装を疑ってみる
と考えるようにしてください。