こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、クラスとモジュール編の次は例外処理編をお送りします。記事中で推奨されている「contingency method」という手法をこのスタイルガイドで初めて知りましたが、今後の例外処理で積極的に検討したいと思います。
Rubyスタイルガイド: 例外処理
6-01【統一】例外処理ではraiseを使う方がfailより望ましい
Prefer
raiseoverfailfor exceptions.
# 不可
fail SomeException, 'message'
# 良好
raise SomeException, 'message'
Rubyリファレンスマニュアルでは、Kernel#raiseとKernel#failは同じページにリンクされており、使い方も同じになっています。どちらもKernelをつけずに呼び出せる関数的メソッドです。
両者に差がないので、スタイル上#raiseに統一するのが目的であると考えられます。
6-02【統一】raiseで引数を2つ与える場合、RuntimeErrorは明示的に指定しない
Don't specify
RuntimeErrorexplicitly in the two argument version ofraise.
# 不可
raise RuntimeError, 'message'
# 良好 - RuntimeErrorはデフォルトで出力される
raise 'message'
#raiseの引数に文字列でメッセージを与えればデフォルトでRuntimeErrorが発生するので、わざわざRuntimeErrorを指定する必要はないということですね。
6-03【統一】raiseでは、例外クラスのインスタンスを引数に渡すのではなく、例外クラスとメッセージを2つの引数として与えるのが望ましい
Prefer supplying an exception class and a message as two separate arguments to
raise, instead of an exception instance.
# 不可
raise SomeException.new('message')
# この方法では`raise SomeException.new('message'), backtrace`を利用できなくなる
# 良好
raise SomeException, 'message'
# `raise SomeException, 'message', backtrace`を利用できる
コメントにあるように、backtraceを利用できるスタイルにします。backtraceについてmorimorihogeさんが補足してくれました。
raise(message) -> ()
raise(error_type, message = nil, backtrace = caller(0)) -> ()
Rubyリファレンスマニュアル: Kernel#raiseの定義より
上の定義から、raiseのデフォルト値であるbacktrace = caller(0)が設定されるには、error_typeとmessageが設定されている必要があるということです。raise(message)のインスタンスだとbacktraceが設定されないことになります。
6-04【統一】ensureブロックでは結果を返さないこと
Do not return from an
ensureblock.
If you explicitly return from a method inside anensureblock, the return will take precedence over any exception being raised, and the method will return as if no exception had been raised at all.
In effect, the exception will be silently thrown away.
ensureブロックで返した結果は、raiseされる例外より優先されてしまいます。これでは例外が発生してない場合と区別できなくなってしまい、例外が警告なしで捨てられてしまいます。
# 不可
def foo
raise
ensure
return 'これはよくないスタイル'
end
6-05【統一】省略可能なbeginブロックは省略する
Use implicit begin blocks where possible.
コード例にもあるように、たとえばメソッド定義全体に対してrescueすることでbegin行とend行が不要になり、インデント階層を深くせずに済みます。
# 不可 -- 階層が余分になる
def foo
begin
# メインのコード
rescue
# 失敗した場合のコード
end
end
# 良好
def foo
# メインのコード
rescue
# 失敗した場合のコード
end
#rescueをdef〜endブロックと同じ最上位インデントに揃えるスタイルについては、好みが分かれるかもしれません。
6-06【統一】「contingency method」でbeginブロックを減らす
Mitigate the proliferation of
beginblocks by using contingency methods
(a term coined by Avdi Grimm).
言葉の説明: contingencyは「不測の事態への備え」、Mitigate the proliferationは「拡散を緩和する」といった意味合いです。
# 不可
begin
something_that_might_fail
rescue IOError
# IOErrorの処理
end
begin
something_else_that_might_fail
rescue IOError
# IOErrorの処理
end
# 良好
def with_io_error_handling
yield
rescue IOError
# IOErrorの処理
end
with_io_error_handling { something_that_might_fail }
with_io_error_handling { something_else_that_might_fail }
contingency methodについて説明がないのでコードから読み取ってみます。beginとendのブロックでrescueでエラーを処理する代わりに、エラー処理ハンドラとなるメソッド(ここではwith_io_error_handling)を定義し、通常処理something_that_might_failを{ }ブロックで与える、という手法(method)ですね。
このようにすることで以下の効果を狙っていると考えられます。
- ファイルの読み書きといった操作のIOError処理を共通化できる
- エラー処理メソッドを呼び出しのブロックで明示的に与えられるので、呼び出しでさまざまなエラー処理メソッドを使い分けられる
beginを書かずに済むのでインデント階層が浅くなる
これはとてもよい例外処理スタイルだと思います。
Contingency methodについて
Contingency methodは、Avdi Grimmによって以下のスライドで提唱された言葉のようです。ページの途中にリンクできなかったので画像を引用しますが、上と同じようなことしか書いてありません。
Exceptional Rubyを読んでみると、このスタイルガイドの「例外処理」の章にある内容の多くがこのスライドを踏まえているようです。Rubyist必読ですね。
6-07【統一】例外を握りつぶさないこと
Don't suppress exceptions.
これはそのままですね。
# 不可
begin
# ここで例外が発生する
rescue SomeError
# rescue文の意味がまったくない
end
# 不可
do_something rescue nil
6-08【統一】後置のrescueは避ける
Avoid using
rescuein its modifier form.
# 不可 - この方法ではStandardErrorクラスとその子孫クラスがキャッチされる
read_file rescue handle_error($!)
# 良好 - この方法ならErrno::ENOENTクラスとその子孫クラスだけがキャッチされる
def foo
read_file
rescue Errno::ENOENT => e
handle_error(e)
end
6-09【統一】例外処理を制御フローに使わないこと
Don't use exceptions for flow of control.
例外処理は通常の制御フローではなく、あくまで例外を処理するためだけに使うということですね。
# 不可
begin
n / d
rescue ZeroDivisionError
puts '0で割ることはできません!'
end
# 良好
if d.zero?
puts '0で割ることはできません!'
else
n / d
end
6-10【統一】Exceptionクラスをrescueすることは避ける
Avoid rescuing the
Exceptionclass. This will trap signals and calls toexit, requiring you tokill -9the process.
Exceptionクラスをrescueするとシグナルがトラップされてexitが呼ばれるので、プロセスをkill -9で止めなければならなくなります。
# 不可
begin
# `exit`が呼ばれてkillシグナルがキャッチされる(`kill -9`を除く)
exit
rescue Exception
puts "まだ終了したくないよね?"
# 例外処理
end
# 良好
begin
# ExceptionではなくStandardErrorからrescueする(「blind」rescue)
# 通常はこの動作が期待される
rescue => e
# 例外処理
end
# これでもよい
begin
# 例外が発生する
rescue StandardError => e
# 例外処理
end
「blind rescue」についてはRuby Worst Practices: Appendix C - Ruby Best Practicesで説明されています。
補足: SignalException
これまたmorimorihogeさんによると、Exceptionクラスをrescueすべきでない理由はSignalExceptionがExceptionのサブクラスに置かれているからだそうです。Signals In Ruby / "rescue Exception" considered harmfulの「Rubys Exception Heirachy」に以下のダンプが掲載されています。
なお、同記事ではException階層のダンプにcheatというgemを使っています。
Exception階層をダンプする方法: Ruby's Exception Hierarchy- cheatのリポジトリ: defunkt/cheat
6-11【統一】詳細な例外処理はrescueチェーンの上位に置き、大域的な例外処理は下位に置くこと
Put more specific exceptions higher up the rescue chain, otherwise they'll never be rescued from.
StandardErrorよりIOErrorの方が詳細なので、IOErrorを後に書くと実行されなくなってしまいます。これはスタイルではなく必須ですね。
# 不可
begin
# 何か書く
rescue StandardError => e
# 例外処理
rescue IOError => e
# この例外処理は実行されない!
end
# 良好
begin
# 何か書く
rescue IOError => e
# 例外処理
rescue StandardError => e
# 例外処理
end
6-12【統一】プログラムで取得した外部リソースは必ずensureブロックで解放すること
Release external resources obtained by your program in an
ensureblock.
f = File.open('testfile')
begin
# 処理を書く
rescue
# エラー処理
ensure
f.close if f
end
次の項とも関連します。
6-13【統一】リソース取得メソッドは、不要になったリソースを自動でクリーンアップするタイプのものを使う
Use versions of resource obtaining methods that do automatic resource cleanup when possible.
# 不可 - ファイルディスクリプタを自分で`close`しないといけない
f = File.open('testfile')
# ファイルで何かする
f.close
# 良好 - ファイルディスクリプタは自動で`close`する
File.open('testfile') do |f|
# ファイルで何かする
end
Markdownの効用のひとつにタグを閉じ忘れずに済むというのがありますが、それと同じように、ブロックの終了とともに自動でcloseするタイプのメソッドの方が安心ですね。
6-14【統一】例外処理には標準ライブラリを使うのが望ましい
Favor the use of exceptions from the standard library over introducing new exception classes.
相当大規模な開発でもなければ、例外処理クラスを新しくこしらえなくてはならない状況はあまりなさそうです。
今回は以上です。次回「コレクション編」もご期待ください。
関連記事
- Rubyスタイルガイドを読む: ソースコードレイアウト(1)エンコード、クラス定義、スペース
- Rubyスタイルガイドを読む: ソースコードレイアウト(2)インデント、記号
- Rubyスタイルガイドを読む: 文法(1)メソッド定義、引数、多重代入
- Rubyスタイルガイドを読む: 文法(2)アンダースコア、多重代入、三項演算子、if/unless
- Rubyスタイルガイドを読む: 文法(3)演算子とif/unless
- Rubyスタイルガイドを読む: 文法(4)ループ
- Rubyスタイルガイドを読む: 文法(5)ブロック、proc
- Rubyスタイルガイドを読む: 文法(6)演算子など
- Rubyスタイルガイドを読む: 文法(7)lambda、標準入出力など
- Rubyスタイルガイドを読む: 文法(8)配列や論理値など
- Rubyスタイルガイドを読む: 命名
- Rubyスタイルガイドを読む: コメント、アノテーション、マジックコメント
- Rubyスタイルガイドを読む: クラスとモジュール(1)構造
- Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど
- Rubyスタイルガイドを読む: クラスとモジュール(3)クラスメソッド、スコープ、エイリアスなど


