例外に悩まされたことはありませんか?
module SomeExceptionalLibrary
class TrivialException < StandardError; end
# 雑にランダムで例外を飛ばすように書いているけど、
# 実際は様々な条件が絡み合ってごく稀に例外が発生してしまうような感じ。
def self.some_brilliant_method
if rand < 0.01
raise TrivialException.new
else
42
end
end
end
require "./some_exceptional_library"
puts SomeExceptionalLibrary.some_brilliant_method
このライブラリはとても素晴らしいライブラリなのですが、時々、例外を発生させてしまいます。
$ ruby my_app.rb
42
$ ruby my_app.rb
42
$ ruby my_app.rb
Traceback (most recent call last):
1: from my_app.rb:3:in `<main>'
/Users/cedretaber/path/to/sample/some_exceptional_library.rb:7:in `some_brilliant_method': SomeExceptionalLibrary::TrivialException (SomeExceptionalLibrary::TrivialException)
例外に対処するのは大変ですよね。
かといって、 例外を握りつぶす のも良くないことと言われています。
では、どうすれば良いのでしょうか?
そうですね。 例外を投げられないようにしてやればいい のですね。
例外なんて投げさせない
こんなコード片を挿入してやりましょう。
require "./some_exceptional_library"
module Kernel
def raise *_args1, **_args2
puts "All right. No problem!"
end
alias fail raise
end
puts SomeExceptionalLibrary.some_brilliant_method
これで例外は発生しません。
$ ruby my_app.rb
42
$ ruby my_app.rb
42
$ ruby my_app.rb
All right. No problem!
お前は何を言っているんだ?
(本気にする人はいないと思いますが)ネタです。プロダクトとかでは絶対にやらないでください。
さて、どうしてこんなことができるのでしょう。
Ruby の予約語一覧を見てみましょう。
next
や return
といった制御構造、 raise
とセットで使うであろう rescue
などは入っていますが、 なんと raise
は予約語ではありません 。
じゃあ例外を投げる時に使っている raise
とは何なのかと言うと、上のコードを見れば一目瞭然で、これは Kernel
モジュールに定義されたメソッドです。そう、単なるメソッドなのです。
そしてご存知の通り、 Ruby はオープンクラスの力を用いることで、組み込みメソッドの挙動すら変更してしまえます。つまり、 raise
の動きは変えることができるのです。
なので、引数を全て無視するメソッドに書き換えてやればこの通り、例外の発生自体を封じ込めることができます。
やった! 我々は例外から解放された!
これ、役に立つの?
たぶん立ちません。
次のようなコードに変えてみましょう。
require "./some_exceptional_library"
module Kernel
def raise *_args1, **_args2
puts "All right. No problem!"
end
alias fail raise
end
puts SomeExceptionalLibrary.some_brilliant_method + 1 # ここ
$ ruby my_app.rb
43
$ ruby my_app.rb
43
$ ruby my_app.rb
All right. No problem!
Traceback (most recent call last):
my_app.rb:11:in `<main>': undefined method `+' for nil:NilClass (NoMethodError)
はい、封じ込めていたはずの例外が逃げ出していますね。
どういうことかというと、 Kernel#raise
を書き換えて防げるのはあくまで「 Ruby コード中の raise
メソッドで投げている例外」で、 C レベルで発生する例外までは防げないのです。まぁ当然ですね。
ここの NoMethodError は Object#method_missing
が発生させている例外ですが、この処理は C で書かれており、 rb_exc_raise
という例外を発生させる関数を直接呼び出しています。なので、いくら Kernel のメソッドを上書きしても駄目なのですね。
「じゃあ Object#method_missing
を書き換えればいいじゃん」となりそうですが、その他にも例外を発生させる組み込みメソッドはいくらでもありますし(例えば零除算で発生する ZeroDivisionError
)、それらにいちいち対応するという生産性のない作業に時間を費やすよりは、例外発生の原因を突き止めて問題を解消する方がよほど有意義でしょう。
例外から逃げてはいけません。
例外と式と文
上で述べたように 例外を握りつぶす という言葉があります。
次のようなコードを書くことを言います。
begin
SomeExceptionalLibrary.some_brilliant_method # 例外が飛ぶかも
rescue
end
投げられた例外を捉えて、 特に何もせず処理を終える ことで、例外を呼び出し元にエスカレーションせずに無視するテクニックです。
テクニックと書きましたが、基本的に例外の握りつぶしは禁じ手と言うか、やらない方が良いことと言われています。私もそう思います。
まぁ、「だったら例外自体を発生させなきゃ良いんだろう」というわけではないのですね。
というか、どこで失敗したのか丁寧に教えてくれる例外はとても尊いものです。例外は決して敵ではありません!
なおこの記事は、Ruby の『文』は return, retry, redo, next, break, alias だけというのを読んで、「あれ raise
は?」と疑問に思い調べてみた結果報告になります。