tldr; raise
の 第一引数に渡したオブジェクトの exception
メソッドの挙動の違いによる。
raise
で例外をメッセージ付きで投げ直した時、その例外の new
が第一引数を取れなくても ArgumentError
にならない
イニシャライザとして引数を受け取らない HogeError
例外を定義する。
class HogeError < StandardError
def initialize
super('hoge error')
end
end
以下は当然 #<HogeError: hoge error>
となる
#<HogeError: hoge error>
begin
raise HogeError
rescue => e
p e
end
HogeError
のイニシャライザは引数を受け取らないので、以下のようにやると ArgumentError
となる
#<ArgumentError: wrong number of arguments (given 1, expected 0)>
begin
raise HogeError, 'argument error?'
rescue => e
p e
end
だけど、 rescue
で受け取った HogeError
のインスタンスをメッセージ付きで raise
し直すと、 ArgumentError
にならない。
#<HogeError: 投げ直し>
def main
raise HogeError
rescue => e
raise e, '投げ直し'
end
begin
main
rescue => e
p e
end
なぜか。
ドキュメントを読んでみる
制御構造 raise - Ruby 3.3 リファレンスマニュアル より
第一引数で指定された例外を、第二引数をメッセージとして発生させます。
んー?これだとこの挙動が起きる理由がよくわかんないな。
module function Kernel.#fail - Ruby 3.3 リファレンスマニュアルより
引数を渡した場合は、例外メッセージ message を持った error_type の示す例外(省略時 RuntimeError)を発生させます。
error_type として例外ではないクラスやオブジェクトを指定した場合、そのオブジェクトの exception メソッドが返す値を発生する例外にします。その際、exception メソッドに引数として変数 message を渡すことができます。
これもなんか微妙な表現だけど、 HogeError#exception
が呼ばれるっぽい。 Exception#exception - Ruby 3.3 リファレンスマニュアルを読んでみよう。
引数を指定した場合 自身のコピーを生成し Exception#message 属性を error_message にして返します。
Kernel.#raise は、実質的に、例外オブジェクトの exception メソッドの呼び出しです。
なるほど! 問題のコードは指定した例外がクラスかインスタンスかの違いがあリ、それがそのまま現れているんだ。 クラスメソッドの Exception.exception
は new
のエイリアスだけど、インスタンスメソッドの Exception#exception
はイニシャライザとは別の動きをして、 message
属性を書き換える動きをする。この挙動の違いだった。
class HogeError < StandardError
def initialize
super('hoge error')
end
end
raise HogeError, 'これは ArugmentError'
raise HogeError.new, 'これはOK'
実装を読んでみる
コアの部分の実装を読むときは TruffleRuby(MRIと97%同じ挙動するらしい) の実装を見ると C が読めない私でも結構読めたりするので見てみた。
Kernel#raise - oracle/truffleruby では Truffle::ExceptionOperations.build_exception_for_raise
を実行して例外を生成している。
def raise(exc = undefined, msg = undefined, ctx = nil, cause: undefined, **kwargs)
cause_given = !Primitive.undefined?(cause)
cause = cause_given ? cause : $!
# 省略...
exc = Truffle::ExceptionOperations.build_exception_for_raise(exc, msg)
ExceptionOperations.build_exception_for_raise - oracle/trufflerubyをみてみると、第一引数 exc
の exception
を呼んでいた。
def self.build_exception_for_raise(exc, msg)
# 省略
exc = exc.exception msg
Exception.exception
は new
と同義なので initialize
を書き換えてると ArgumentError
になるけど、 Exception#exception
は new
とは別の動きをするのでエラーにならない。
class << self
alias_method :exception, :new
end
def exception(message = nil)
# As strange as this may seem, this is actually the protocol that CRuby implements
if message and !Primitive.equal?(message, self)
copy = clone # note: rb_obj_clone() in CRuby
Primitive.exception_set_message copy, message
copy
else
self
end
end
Exception.exception, Exception#exception - oracle/truffleruby
その通りの実装だ!!