QA@IT

Ruby の SimpleDelegator を継承したクラスで method_missing を定義した場合の挙動を教えてください

3458 PV

前提の説明

Ruby のプログラムで、特定の環境変数の値を得るコードを簡潔に書きたいので、 OpenStruct と SimpleDelegator を使って以下のようなコードを書きました。

require 'delegate'
require 'ostruct'

class MyProxyClass < SimpleDelegator
  def initialize(env)
    super(OpenStruct.new(env))
  end

  def method_missing(method, *args)
    super
  rescue NoMethodError
    super(method.upcase, *args)
  rescue NoMethodError
    nil
  end
end

このクラスは以下のようにして利用します。

env_proxy = MyProxyClass.new(ENV)
p env_proxy.USER
p env_proxy.user
p env_proxy.unknown_environment_variable
  • OpenStruct で ENV をラップすれば env_proxy.USER のようにアクセスできる
  • env_proxy.user と小文字でもアクセスできたほうが嬉しいので、 method_missing を再定義して .user で値を取得できなかった場合は .USER を改めて呼び出すようにした

ここまでは良かったのですが、未定義の環境変数にアクセスしようとすると NoMethodError 例外が発生してしまいます。 rescue 節を増やして明示的に nil を返すようにしてもやはり例外が発生してしまいます。

実行結果は以下です。

$ ruby test_delegate.rb
"usr0600121"
"usr0600121"
test_delegate.rb:12:in `rescue in method_missing': undefined method `UNKNOWN_ENVIRONMENT_VARIABLE' for #<MyProxyClass:0x007fdad200f4b0> (NoMethodError)
        from test_delegate.rb:10:in `method_missing'
        from test_delegate.rb:21:in `<main>'

聞きたいこと

  • なぜ OpenStruct でラップしたオブジェクトが知らないメソッドを呼び出すと NoMethodError 例外が発生するのか?
    • そもそも OpenStruct でラップしたオブジェクトに直接 env.unknown_environment_variable などとすると nil が返るので、それをそのまま返して欲しいのにそうならないのは何故か?
  • なぜ SimpleDelegator を継承したクラスの method_missingNoMethodError を補足しているのに例外が発生するのか?

実行環境について

$ uname -a
Darwin PMAC046J.local 12.2.0 Darwin Kernel Version 12.2.0: Sat Aug 25 00:48:52 PDT 2012; root:xnu-2050.18.24~1/RELEASE_X86_64 x86_64 i386 MacBookAir4,2 Darwin

$ ruby -v
ruby 1.9.3p392 (2013-02-22 revision 39386) [x86_64-darwin12.2.0]

コードと実行結果は Gist にもアップロードしています https://gist.github.com/kyanny/26b5cb9b3e06b982be54

回答

  • なぜ OpenStruct でラップしたオブジェクトが知らないメソッドを呼び出すと NoMethodError 例外が発生するのか?
    • そもそも OpenStruct でラップしたオブジェクトに直接 env.unknown_environment_variable などとすると nil が返るので、それをそのまま返して欲しいのにそうならないのは何故か?

OpenStructのインスタンスは、知らないメソッドを呼ばれるとnilを返しますが、respond_to?にはfalseを返します。
そして、Delegatorは委譲先のメソッドを呼び出す前にまずrespond_to?を呼び、それがtrueであることを確認してからメソッドを呼ぶようになっています。もしfalseの場合はsuperするので、BasicObjectにないメソッドの場合はそこでNoMethodError例外となる、というわけです。

ですから、OpenStructのnilをそのまま返してほしいのであれば、OpenStructのrespond_to?をオーバーライドする必要があります。
しかし、それでは「小文字がないとき大文字」の処理ができなくなるので、method_missingをオーバーライドするという方針でいいと思います。

  • なぜ SimpleDelegator を継承したクラスの method_missingNoMethodError を補足しているのに例外が発生するのか?

これは、begin - rescue - end の形とするとbeginからrescueの間の例外しか捕捉されないので、このようにrescue節内でふたたびrescueしてやればいいです。

  def method_missing(method, *args)
    super
  rescue NoMethodError
    begin
      super(method.upcase, *args)
    rescue NoMethodError
      nil
    end
  end
編集 履歴 (1)
  • ありがとうございます。 respond_to? がミソだったのですね。全く思いつきもしませんでした。

    結局 delegate を使うのを諦めて https://gist.github.com/kyanny/6d3113bf8a403f3885b1 のようにしてやりたいことはできてしまったのですが、とても勉強になりました。 rescue の件はうっかりしてました...
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。