ensureでreturnすると...
rubyで例外処理を書くとき、ensureに「return」を書いてはいけません。
# ヤバい例
def div(x, y)
puts "=== 計算開始 ==="
ret = {}
ret[:answer] = x / y
ret[:message] = "割り算成功!"
rescue ZeroDivisionError => e
ret[:answer] = nil
ret[:message] = "割り算失敗..."
ensure
puts "=== 計算終了 ==="
return ret
end
なぜなら、ensureでreturnすると、rescueで指定していない例外ももみ消すという恐ろしい挙動をするからです。
下記の実行例を見てください。
# 実行例1
div(10,2)
=== 計算開始 ===
=== 計算終了 ===
#=> {:answer=>5, :message=>"割り算成功!"}
# 実行例2
div(10,0)
=== 計算開始 ===
=== 計算終了 ===
#=> {:answer=>nil, :message=>"割り算失敗..."}
# 実行例3
div(10,"abc")
=== 計算開始 ===
=== 計算終了 ===
#=> {}
実行例1・2は、想定どおりの動作かも知れません。
でも、実行例3はどうでしょうか?
「10 ÷ "abc"」は計算できないので、TypeErrorが発生しそうに思えますが、実際は例外が発生せず、空のハッシュが返されています。
このようなコードは、出るべき例外を隠してしまうので、バグの温床となる上、デバッグがしづらくなり、とても厄介です。
改善例
ensureでreturnするのではなく、本体とrescueの最後に、戻り値に指定したい値を書くようにしましょう。
# 改善例
def div(x, y)
puts "=== 計算開始 ==="
ret = {}
ret[:answer] = x / y
ret[:message] = "割り算成功!"
return ret
rescue ZeroDivisionError => e
ret[:answer] = nil
ret[:message] = "割り算失敗..."
return ret
ensure
puts "=== 計算終了 ==="
end
# 実行例1
div(10,2)
=== 計算開始 ===
=== 計算終了 ===
#=> {:answer=>5, :message=>"割り算成功!"}
# 実行例2
div(10,0)
=== 計算開始 ===
=== 計算終了 ===
#=> {:answer=>nil, :message=>"割り算失敗..."}
# 実行例3
div(10,"abc")
=== 計算開始 ===
=== 計算終了 ===
TypeError: "abc" can't be coerced into Integer
実行例1・2の動作は変わらず、実行例3の問題が解決されているのが分かると思います。
補足:言語仕様について
divメソッドは、必ず最後に
puts "=== 計算終了 ==="
を処理するため、戻り値がnilになってしまうのでは、と考える方もいるかも知れませんが、心配無用です。
rubyは、言語仕様として、ensure節の評価値を無視するようになっているからです。
begin式全体の評価値は、本体/rescue節/else節のうち 最後に評価された文の値です。また各節において文が存在しなかったときの値 はnilです。いずれにしてもensure節の値は無視されます。
ちなみに、上記の仕様は、「Ruby技術者認定試験 Silver」でも問題として出題されます。
Ruby技術者認定試験で問われる知識は、他にも実践で役立つものが多い印象です。
ruby初心者で、まだ受けたことのない人は、是非1度受けてみると良いと思います。