[Ruby] sendとmethod_missingと分かりづらい仕様
つい最近、腰痛で一時動けなかくつらい思いを久々にしました。
昔痛めて生涯の友となった腰痛ですが、数年に一回来るので辛いです。
さて、最近はRubyでとあるライブラリを作成しているのですが、黒魔術を使わざるを得ない状況はどうしても出てくるものです。
そこで不可解なエラーに遭遇しました。
コードを単純化するとこうなります。
class Test def method_missing(method, *args) Test1.new.send(method, *args) end end class Test1 def method_missing(method, *args) end end Test.new.select(1)
実行するとなぜかエラー。
$ ruby hoge.rb hoge.rb:3:in `select': wrong argument type Integer (expected Array) (TypeError) from hoge.rb:3:in `method_missing' from hoge.rb:12:in `<main>'
試しに他の存在するメソッド(例えば__id__とか)を呼ぶと問題は無い。
Test、Test1クラスはObjectクラスを継承しただけなので、ほぼ同じ。
https://docs.ruby-lang.org/ja/latest/class/Object.html を見るとselectは存在しないはず...
仕方なく同僚に相談して、二人で悩んで解決しました
ちなみにTest1を使用しなくてもこれで反応します。
class Test def method_missing(method, *args) send(method, *args) end end Test.new.select(1)
まず、見るべきはsendの仕様。
https://docs.ruby-lang.org/ja/latest/class/Object.html#I___SEND__
send, __send__ は、メソッドの呼び出し制限 にかかわらず任意のメソッドを呼び出せます。
メソッドの呼び出し制限 にかかわらず??
そして、private methodを見るとselectが存在する!
> Object.new.private_methods.sort
=> [:Array, :Complex, :DelegateClass, :Float, :Hash, :Integer, :Rational, :String, :__callee__, :__dir__, :__method__, :`, :abort, :at_exit, :autoload, :autoload?, :binding, :block_given?, :caller, :caller_locations, :catch, :default_src_encoding, :eval, :exec, :exit, :exit!, :fail, :fork, :format, :gem, :gem_original_require, :gets, :global_variables, :initialize, :initialize_clone, :initialize_copy, :initialize_dup, :irb_binding, :iterator?, :lambda, :load, :local_variables, :loop, :method_missing, :open, :p, :print, :printf, :proc, :putc, :puts, :raise, :rand, :readline, :readlines, :require, :require_relative, :respond_to_missing?, :select, :set_trace_func, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined, :sleep, :spawn, :sprintf, :srand, :syscall, :system, :test, :throw, :trace_var, :trap, :untrace_var, :warn]
つまり、この流れでエラーとなっているようです
- sendはprotect、privateであっても呼び出しが可能
- Object#selectがprivateメソッドとして用意されている
- よって、Test1#method_missingを呼ぶ前にselectメソッドを見つけてエラーとなる
さて、問題はどうするか?ということですが、メソッドのリストをよく見るとpublic_sendというのがあるようです
sendをpublic_sendにすると問題なく呼ばれたようで解決しました。
class Test def method_missing(method, *args) Test1.new.public_send(method, *args) end end
初めはRubyのバグを踏んだ?と思いましたが、よくよく調べると仕様通りの動作ですがなかなかややこしい問題でした。
黒魔術はほどほどに...