コードレビューで自分が早期リターンがあんまり好きじゃないなぁという話をして、そういえばなんであんまり好きじゃないんだろうと自問自答してみたことをまとめてみます。
好き嫌いの範疇なので、早期リターンが絶対悪だとは思いませんし、絶対善だとも思いません。使おうが使うまいが好きにすればいいと思いますし、使うといいシチュエーションも、使うとイマイチなシチュエーションもあると思います。
自分なりにOKなパターン
ガード節
早期リターンするからこそのガード節ですから、これはOKです。
def func(params)
return if (おかしなパラメータ判定)
:
end
メモ化
メモ化も早期リターンするからこそ速度メリットがでるわけですから、まあOKですね。
def func
return @_func if defined?(@_func)
@_func = (なんか処理)
end
でもこう書きたいですけどね。なんか処理
が nil 返すとメモ化出来ないって罠がありますけれど。
def func
@_func ||= (なんか処理)
end
エラー処理
エラー時もそれ以上処理を続ける意味がありませんから早期リターンしていいと思います。普通は例外処理でやっちゃいますから、エラーでリターンするって事も今どきはないですけどね。
def func
result = (なんか処理)
return unless result
:
end
自分なりにイマイチなパターン
同格の処理を同格に扱ってない
条件によって処理Aと処理Bのどちらかを実行する場合があったとします。
def func
if (条件)
(処理A)
else
(処理B)
end
end
これを早期リターンでは以下のように書くわけです。
def func
if (条件)
(処理A)
return
end
(処理B)
end
早期リターンだと処理Aと処理Bを同格に扱ってないのが自分としては好きではない。コードから、処理Aと処理Bが同格であるという情報が欠落してしまっている。
論理が反転する
def func
:
if (条件)
(処理)
end
end
を早期リターン使うと
def func
:
return unless (条件)
(処理)
end
って書くわけですが、条件分岐の論理が反転するので、それを脳内で補完するのが面倒です。
条件と処理が結びついているとしたら、それが分離されてしまうのもイマイチです。
早期リターンのメリットに対して
インデント浅くできる
まあわかる。インデントがめっちゃ深くなってるなら、早期リターンで浅くするってのもやむ無しかな。でもそれってもしかしてメソッド分割するべきってサインなのかもしれないとも思ったり。
ここから先はもう読まなくていいと判断出来て脳の負担が軽くなる
早期リターンできる条件の処理だけを追ってるならそうだけど、メソッド全部を理解しなきゃいけないなら楽になってないと思う。
例を追加
(2021.10.1追記)
ただの蛇足ですが、まあ例は多い方が分かりやすいかなと。
例えば次のコードがあったとします。
def func n
case n
when 1
"one"
when 2
"two"
else
"other"
end
end
早期リターンだとこんな感じに書き変えるわけですが、個人的にはこれが読みやすくなったとは思えません。例が悪いかもしれませんが。
def func n
return "one" if n == 1
return "two" if n == 2
"other"
end
そもそもこの例ですとハッシュテーブル使った方が分かりやすいとは思いますけれどね。
def func n
{1 => "one", 2 => "two"}[n] || "other"
end
更に例を追加
(2022.4.29追記)
最後の条件にしか適用できない
def func
if (条件A)
(処理A)
end
if (条件B)
(処理B)
end
if (条件C)
(処理C)
end
end
複数の条件/処理があっても、早期リターンは 条件C/処理C にしか適用できません。これ、ほんとにコードが見やすくなってますか。
def func
if (条件A)
(処理A)
end
if (条件B)
(処理B)
end
return unless (条件C)
(処理C)
end
後ろに処理を追加できない
def func
if (条件)
(処理)
end
end
を早期リターンを使って書き換えたとします。
def func
return unless (条件)
(処理)
end
ここで func の最後に常に行わないといけない処理が追加される場合どうなるでしょう。元のコードではただ単に処理を追加するだけですね。
def func
if (条件)
(処理)
end
(追加処理)
end
一方、早期リターンを使ってる場合はこんなことになってしまいます。
def func
unless (条件)
(追加処理)
return
end
(処理)
(追加処理)
end
更に更に例を追加
(2022.5.19追記)
大域脱出は個人的にOKです。そもそも大域脱出を早期リターンとは言わないような気もしますけど。
def func
10.times do |i|
10.times do |j|
return (なんか条件)
end
end
end
そして今回ぐぐって初めて知ったんですが、Ruby には大域脱出メソッドがあるんですね。
まとめ
(2022.5.19追記)
いろいろ例を挙げてみて自分なりの考えが少し整理できたのでまとめておきます。
まず前提としてあるのは、一つのメソッドは一つの機能だけを実現するということです。二つ以上の機能をまとめちゃうと、それはスパゲッティまっしぐらですので推奨されません。
一つの機能の前処理として、パラメータが不正であったりとか、準備処理に失敗したとかでリターンするのは構わないと思います。でもそれは自分の中では早期リターンとは別物という扱いです。
メソッドが一つの機能だけを実現しているのに途中でリターンできてしまうということは、それは実は機能は一つではなく複数あるのではないか、という疑いですね。実は複数機能あるのだとしたら、やるべきことは早期リターンではなくメソッド分割でしょう。
もちろんこれは新規に自分が書き下ろすコードの場合の話です。引き継いだコードが巨大な一枚岩メソッドになっていて、リファクタリングするためにとりあえず早期リターンを入れていくなどはアリだと思います。