インライン rescue を使う時の注意点
Ruby の rescue
をインラインで, つまりは statement modifier として使う時の注意点を紹介します.
Ruby のバージョンは 2.5.1
になります.
インライン rescue
は, 節 rescue
の速記
rescue
を次のように statement modifier として使うと:
raise rescue puts 'RuntimeError occured'
raise
は Runtime Error
を発生させるので, RuntimeError
は StandardError
のサブクラスなので, StandardError
とそのサブクラスの全ての例外を rescue
はキャッチするので, 次のような結果になります:
RuntimeError occured
つまりは次のような begin
文の節として使う場合の速記になります:
begin
raise
rescue
puts 'RuntimeError occured'
end
このように rescue
を例外クラスを何も指定しないで使う場合, インラインの rescue
の方がより簡潔に書くことができるためそのようなメリットがあるのですが, 思わぬ落とし穴があったりします.
インライン rescue
の落とし穴
例えば次のような 2 分の 1 の確率で odd
を返し, 2 分の 1 の確率で raise
するメソッドを定義したとします:
def ood_or_even
rand(2) == 0 ? 'odd' : raise
end
10.times do
puts odd_or_even rescue puts 'even'
end
そのメソッドを 10.times
のブロック内で呼び出して, 例外が発生しなかったら odd
, 発生したら even
を 10 回, 2 分の 1 の確率でそれぞれ表示すると一見すると思われますが, 実際の結果は次のようになります:
even
even
even
even
even
even
even
even
even
even
10 回中 10 回とも全て even
です. rand(2)
で例外が発生する確率は 2 分の 1 の筈なのにそんなことってあり得るのと思われるかも知れません.
もしかしたら 2 の 10 乗である 1024 分の 1 の確率を引いたのかと, 思われるかも知れませんが, 10.times
を 100.times
としても, はたまた 1000.times
としても表示されるのは全て even
になります:
100.times.map { odd_or_even rescue 'even' }.all? 'even' # => true
1000.times.map { odd_or_even rescue 'even' }.all? 'even' # => true
つまりは確率の問題ではありません.
ここに rescue
をインラインで使った時の落とし穴があります.
rescue
は最初の方でも書きましたが, StandardError
とそのサブクラスの全ての例外をキャッチします.
なので次の行の:
puts odd_or_even rescue puts 'even'
rescue
がキャッチする例外は必ずしも RuntimeError
だけではないのです.
実は RuntimeError
以外の例外が 100% の確率で発生していたので, そのように even
が 10 回中 10 回とも表示されたのです.
RuntimeError
以外の例外とは一体何なのかを見つけるために先ほどのコードを次のように変更してみます:
10.times do
puts odd_or_even rescue p $!
end
$!
は raise
によって発生した例外情報が入ってします.
そして実行すると, 次のように表示されます:
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
このように正体は NameError
でした.
メッセージを見ると, odd_or_even
というメソッドが定義されていないという事がわかります.
先ほどのコードを見ていただけると odd_or_even
というメソッドを定義したつもりが ood_or_even
となってしまってします.
この NameError
が RuntimeError
の前に発生していたので, 先ほどのような全て even
という結果になりました.
なのでその ood_or_even
メソッドの名前を odd_or_even
と直して定義し直すと, ちゃんと想定通りの結果が表示されます:
odd
even
odd
even
even
odd
even
even
even
even
特定の例外を拾う場合は rescue
を節として使う
このように rescue
をインラインで使ってしまうと, 想定している例外も拾ってくれますが, 想定していない例外も同様に拾ってしまうので, 先ほどの 10.times
のブロック内のコードは RuntimeError
という特定の例外を拾うために次のように書くといいです:
10.times do
puts odd_or_even
rescue RuntimeError
puts 'even'
rescue
p $!
end
実行すると NameError
が発生したと知る事ができます:
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean? ood_or_even>
次の 2 行は, 例外情報をわかりやすく表示させるために付け足したものなので, 削除しても全く問題ありません:
rescue
p $!
このように特定の例外クラスを raise
してその特定の例外を拾う必要がある場合は, インラインの rescue
ではなく, 節としての rescue
を使われた方がいいです.
まとめ
Ruby コミッタを務められている, 卜部昌平さんは rescue
修飾語句 (インライン rescue
のこと) は begin
と end
の節として使う rescue
と比べて使いやすすぎると感じられていて, その短さ故にその修飾語句スタイルは不適切に使われがちと述べられています.1
そして rescue
修飾語句を使う場合は, rescue
の左側は式で, 右側は nil
や false
のような副作用がない使い方に限定するコーディングスタイルを個人的に提案されています.
なのでそのようなコーディングスタイルで書けないような rescue
は, 文の節として, キャッチする特定の例外クラス付きで定義した方がいいのでしょう.
関連記事
Ruby の正規表現を備忘録としてまとめてみた2018.08.30
Ruby のクラスやメソッドのドキュメントを見れる ri コマンドが便利な件2018.09.08
Vim プラグイン ri.vim で Ruby のドキュメントを Vim の中でも見れてしまう2018.09.13
Ruby のグローバル変数をそれぞれ調べてみた2018.06.12
Ruby のメソッドチェインって便利で楽しい2018.05.04