Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: 例外処理

こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、クラスとモジュール編の次は例外処理編をお送りします。記事中で推奨されている「contingency method」という手法をこのスタイルガイドで初めて知りましたが、今後の例外処理で積極的に検討したいと思います。

Rubyスタイルガイド: 例外処理

6-01【統一】例外処理ではraiseを使う方がfailより望ましい

Prefer raise over fail for exceptions.

# 不可
fail SomeException, 'message'

# 良好
raise SomeException, 'message'

Rubyリファレンスマニュアルでは、Kernel#raiseKernel#failは同じページにリンクされており、使い方も同じになっています。どちらもKernelをつけずに呼び出せる関数的メソッドです。

両者に差がないので、スタイル上#raiseに統一するのが目的であると考えられます。

6-02【統一】raiseで引数を2つ与える場合、RuntimeErrorは明示的に指定しない

Don't specify RuntimeError explicitly in the two argument version of raise.

# 不可
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_typemessageが設定されている必要があるということです。raise(message)のインスタンスだとbacktraceが設定されないことになります。

6-04【統一】ensureブロックでは結果を返さないこと

Do not return from an ensure block.
If you explicitly return from a method inside an ensure block, 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

#rescuedefendブロックと同じ最上位インデントに揃えるスタイルについては、好みが分かれるかもしれません。

6-06【統一】「contingency method」でbeginブロックを減らす

Mitigate the proliferation of begin blocks 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について説明がないのでコードから読み取ってみます。beginendのブロックでrescueでエラーを処理する代わりに、エラー処理ハンドラとなるメソッド(ここではwith_io_error_handling)を定義し、通常処理something_that_might_fail{ }ブロックで与える、という手法(method)ですね。

このようにすることで以下の効果を狙っていると考えられます。

  • ファイルの読み書きといった操作のIOError処理を共通化できる
  • エラー処理メソッドを呼び出しのブロックで明示的に与えられるので、呼び出しでさまざまなエラー処理メソッドを使い分けられる
  • beginを書かずに済むのでインデント階層が浅くなる

これはとてもよい例外処理スタイルだと思います。

Contingency methodについて

Contingency methodは、Avdi Grimmによって以下のスライドで提唱された言葉のようです。ページの途中にリンクできなかったので画像を引用しますが、上と同じようなことしか書いてありません。


Exceptional Rubyより

Exceptional Rubyを読んでみると、このスタイルガイドの「例外処理」の章にある内容の多くがこのスライドを踏まえているようです。Rubyist必読ですね。

6-07【統一】例外を握りつぶさないこと

Don't suppress exceptions.

これはそのままですね。

# 不可
begin
  # ここで例外が発生する
rescue SomeError
  # rescue文の意味がまったくない
end

# 不可
do_something rescue nil

6-08【統一】後置のrescueは避ける

Avoid using rescue in 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 Exception class. This will trap signals and calls to exit, requiring you to kill -9 the 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すべきでない理由はSignalExceptionExceptionのサブクラスに置かれているからだそうです。Signals In Ruby / "rescue Exception" considered harmfulの「Rubys Exception Heirachy」に以下のダンプが掲載されています。


Signals In Ruby / "rescue Exception" considered harmfulより

なお、同記事ではException階層のダンプにcheatというgemを使っています。

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 ensure block.

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.

相当大規模な開発でもなければ、例外処理クラスを新しくこしらえなくてはならない状況はあまりなさそうです。

今回は以上です。次回「コレクション編」もご期待ください。

関連記事


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。