初心者がつまずくRubyのraiseとthrowについてちょうどいい記事がなかったのでまとめました。
raise
raiseについて軽くおさらいします。C++、Javaなどのtry/throw/catchに近い
raiseできるのはExceptionクラスを継承したクラス/インスタンスのみ
raise Fixnum #=> TypeError: exception class/object expected
raise 5 #=> TypeError: exception class/ object expected
ExceptionにはSignalExceptionやScriptErrorなども含まれるので、何も考えずrescue Exceptionなどと書いてしまうと、スクリプトを強制終了したいときや構文エラーのときもrescueされてしまう。大抵はStandardError以下を使う。
引数なしrescueはrescue StandardErrorと同値
def default_rescue
raise StandardError
rescue # StandardError
puts '救われたよかった'
end
default_rescue #=> "救われたよかった"
また、rescueされるオブジェクトには、サブクラスとインスタンスも含まれる。
# 全部無印rescueでも処理されるよ
raise StandardError # StandardErrorクラスそのもの
raise StandardError.new # StandardErrorのインスタンス
raise ArgumentError # StandardErrorのサブクラス
4 / 0 # ZeroDivisionErrorもStandardErrorのサブクラス
同じ階層に複数置けるよ
begin
raise NameError
rescue NameError
puts 'name'
rescue StandardError
puts 'standard'
end # この場合"name"が表示される
else、ensure、fail、単体raiseや、返り値についてにも書きたいが、割愛
重要なのは「エラーメッセージやスタックトレースを表示するためのもの」だということ
begin
raise ArgumentError.new('引数が違う')
rescue ArgumentError => e
puts e.message
puts e.backtrace.inspect
end
throw
あまり直感的ではないけど、Rubyでのthrowの用途はCなどでいうgotoに近い。制御フローに使う。
なんでも投げれる1
catch Object do
throw Object
end
ただし、throwするオブジェクトはcatchするオブジェクトと全く同じものでなくてはならない。==や===で比較もかけてくれない。
catch "sample" do
throw "sample"
end
# 内容は一緒でも違うオブジェクトなのでcatchされない
# "sample".object_id != "sample".object_id
因みに、catchに失敗したときUncaughtThrowErrorが吐かれる2が、これはrescue可能。両者の違いをよく理解してない人が聞くと「ん?」となる瞬間(らしい)。
また、catchしたオブジェクトを参照することもできない。代わりに二つ目の引数で返り値を指定する。
これらの理由から、結局普通はSymbolを使う。
def find_something
# stub
throw :found, "found!"
end
puts "catch前"
msg = catch(:found) do
puts "throw前"
find_something
puts "throw後" # 実行されないよ
end
puts msg
puts "catch後"
# catch前
# throw前
# found!
# catch後
あとは、rescueと違って複数catchしたい場合はネストする必要があります。
結局何が違うの
ここまで読めば大体わかると思いますが、両者はまったく別物です。
用途が違う
raise..rescueはエラーハンドリングのためでした。throw..catchに例外/エラーは関係ありません。スタックから素早く抜け出したいときに使えます。
パフォーマンスが違う
raiseはバックトレースをメモりながら戻ってくるのに対して、throwはジャンプするだけなので、実行時間が全然変わってきます。
愛され方が違う
raise..rescueはよく使われるの対してthrow..catchは誰も使ってくれません。当たり前なんですが、それにしてもgotoが悪とされてるからって敬遠してる人が多いので、以下に例を挙げます
そもそもthrowって何に使うの
ここまで読むとthrowは忘れてraiseだけを知っておけばいいみたいになりそうなので..
throwの一番よく見るユースケースは深いループから素早く抜け出すときです。
catch :out do
while true
while true
while true
while true
throw :out, "脱出"
end
end
end
end
end #=> "脱出"
そもそもこういうコードを書いている時点で設計がおかしいという議論がありますが、現実では複数breakが欲しくなる状況がごく稀にでてきますし、知っててもいいんじゃないかと
関数にしちゃってreturnで抜け出しても同じことができるんじゃないかという議論もあります。そうですね。でもスコープが変わっちゃうしたまにはthrowも使ってね
gotoのようなことをしているのにインデントのおかげでそこまで読みにくくないというのがthrowの良いところだと思います。
また、有名どころではSinatraがhaltをcatch/throwを使って実装しています。3 halt使ってる人はthrowに優しくしてあげて
# Exit the current block, halts any further processing
# of the request, and returns the specified response.
def halt(*response)
response = response.first if response.length == 1
throw :halt, response
end
結論
エラーハンドリングにはraise..rescueを使いましょう。
throw..catchはgotoのような制御構文ですが、gotoのワルなところをできるだけ削ったマルいやつになってます。いつか使うときがくるかも分からないので一応覚えておくといいかも。