LoginSignup
4
2

Rubyの raise で例外をメッセージ付きで投げ直した時、その例外の new が第一引数を取れなくてもArgumentErrorにならないのはなぜか

Last updated at Posted at 2024-06-07

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.exceptionnew のエイリアスだけど、インスタンスメソッドの 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をみてみると、第一引数 excexception を呼んでいた。

    def self.build_exception_for_raise(exc, msg)
      # 省略
      exc = exc.exception msg

Exception.exceptionnew と同義なので initialize を書き換えてると ArgumentError になるけど、 Exception#exceptionnew とは別の動きをするのでエラーにならない。

  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

その通りの実装だ!!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2