pavlog

ウェブエンジニアがあれやこれやを書きます

Ruby: 例外処理とrescue/ensureの挙動について

f:id:paveg:20190126044738j:plain

プログラムの挙動が、期待通りにいかないことはよくあることだと思います。 そんな時に臨時で処理を行うものとして、例外処理がRubyには存在します。

基本的には例外処理はあまり使わないように実装すべきだと考えています。

しかしながら、得てして既に実装済みのコードなどで例外処理は存在するので、使わざるを得ないケースが存在します。

今回は、Rubyの例外処理の挙動を備忘録としてまとめておきます。

・目次

例外処理の挙動

Gemと検証用のクラスを準備する

検証を行うために、まずはじめに pry という Rubygemをインストールしておきます。

❯ gem install pry
Successfully installed pry-0.12.2
Parsing documentation for pry-0.12.2
Done installing documentation for pry after 1 seconds
1 gem installed

github.com

そして、下記のような ExceptionTest クラスを準備しておきます。

[2] pry(main)> show-method ExceptionTest

From: (pry) @ line 1:
Class name: ExceptionTest
Number of lines: 21

class ExceptionTest
  def test_rescue!
    raise_zero_division_error!
  rescue => ex
    puts ex.message
  end

  def test_ensure!
    raise_zero_division_error!
  rescue => ex
    puts ex.message
  ensure
    puts 'ensure'
  end

  private

  def raise_zero_division_error!
    1 / 0 # raise ZeroDivisionError
  end

  def raise_runtime_error!
    raise RuntimeError
  end
end

begin - rescue - end

準備したクラスを動かしていきます。

まず、テストする対象は、 ExceptionTest#test_rescue! の挙動です。

ExceptionTest クラスのインスタンスメソッドである test_rescue! が呼ばれるとZeroDivisionError が発生して、 返り値は nil を返します。

では、実際に動かしてみます。

[3] pry(main)> et = ExceptionTest.new
=> #<ExceptionTest:0x00007ff842404588>
[4] pry(main)> et.test_rescue!
divided by 0
=> nil
[5] pry(main)>

想定通りの例外出力となりました。これは特に問題ないと思います。

また rescue で例外を捕捉して、例外毎に処理を分けることも可能です。

ExceptionTest を下記のように変更して実行してみます。

[7] pry(main)> show-method ExceptionTest.new.test_rescue!

From: (pry) @ line 2:
Owner: ExceptionTest
Visibility: public
Number of lines: 12

def test_rescue!
  2.times do |i|
    begin
      raise_zero_division_error! if i == 0
      raise_runtime_error!
    rescue StandardError => ex
      puts "#{ex.message}, iteration: #{i}"
    rescue RuntimeErro => ex
      puts "#{ex.message}, iteration: #{i}"
    end
  end
end

実行結果は以下のようになります。

Integer#times なので、返り値が nil ではなくなっています。

[8] pry(main)> et = ExceptionTest.new
=> #<ExceptionTest:0x00007fe7a94b6390>
[9] pry(main)> et.test_rescue!
divided by 0, iteration: 0
RuntimeError, iteration: 1
=> 2

begin - rescue - ensure - end

ensure の挙動 は ドキュメントに下記のように記載してあります。

ensure 節が存在する時は begin 式を終了する直前に必ず ensure 節の本体を評価します。

begin式全体の評価値は、本体/rescue節/else節のうち 最後に評価された文の値です。また各節において文が存在しなかったときの値 はnilです。いずれにしてもensure節の値は無視されます。

制御構造 (Ruby 2.6.0)

これによると、 ensure を定義した場合には、例外処理の如何に関わらず必ず評価されるはずです。

[10] pry(main)> et.test_ensure!
divided by 0
ensure
=> nil

rescue で捕捉した例外を ensureで扱えるか

ensure を評価する時に rescue で捕捉した exception がどのように扱われるか確認します。

下記のように書き換えます。

[11] pry(main)> show-method et.test_ensure!

From: pry-redefined(0x3ff3d4c98f94#test_ensure!) @ line 1:
Owner: ExceptionTest
Visibility: public
Number of lines: 7

def test_ensure!
  raise_zero_division_error!
rescue => ex
  puts ex.message
ensure
  puts "#{ex.message} in ensure"
end

実行結果では、 rescue で捕捉した exceptionensure でも同様の変数名で扱えるようです。

[12] pry(main)> et.test_ensure!
divided by 0
divided by 0 in ensure
=> nil

おさらい

  1. rescue で例外を捕捉して、任意の処理を実行できる
  2. 例外処理における実装では、例外の範囲を小さくするべき
  3. ensure 節は、 rescue の実行有無に関係なく必ず評価される
  4. rescue で捕捉した例外については、 ensure で扱うことが可能
  5. rescueexception を入れないケースについては変数を呼び出すと例外( NameError )を引き起こすので注意する
  6. 例外を起こさないような実装を心掛けましょう