新手问题 《Effective Ruby》有误?:define_singleton_method 报 warning: instance variable @object not initialized

LinuxGit · 2016年04月04日 · 最后由 imconfused 回复于 2016年04月07日 · 3018 次阅读
require 'logger'

class AuditDecorator
  def initialize (object)
    @object = object
    @logger = Logger.new(STDOUT)

    @object.public_methods.each do |name|
      define_singleton_method(name) do |*args, &block|

        @logger.info("calling '#{name}' on #{@object.inspect}")
        @object.send(name, *args, &block)
      end
    end
  end
end

s1 = AuditDecorator.new("ruby!")
#puts s1.downcase
puts s1.class

运行报错:

I, [2016-04-04T22:07:43.357305 #1575]  INFO -- : calling 'define_singleton_method' on "ruby!"
I, [2016-04-04T22:07:43.357316 #1575]  INFO -- : calling 'define_singleton_method' on "ruby!"
audit_decorator.rb:11: warning: instance variable @logger not initialized
audit_decorator.rb:11: warning: instance variable @object not initialized
audit_decorator.rb:11:in `block (2 levels) in initialize': undefined method `info' for nil:NilClass (NoMethodError)
    from audit_decorator.rb:12:in `block (2 levels) in initialize'
    from audit_decorator.rb:9:in `block in initialize'
    from audit_decorator.rb:8:in `each'
    from audit_decorator.rb:8:in `initialize'
    from audit_decorator.rb:18:in `new'
    from audit_decorator.rb:18:in `<main>'

求助,谢谢,不知道问题出在哪。书中例子是通过 define_singleton_method 实现 decorator pattern.

require 'logger'


class AuditDecorator
  def initialize (object)
    @object = object
    @logger = Logger.new(STDOUT)

    meths = @object.public_methods
    meths.delete(:define_singleton_method)
    meths.push(:define_singleton_method)
    meths.each do |name|
      define_singleton_method(name) do |*args, &block|
        @logger.info("calling '#{name} with args(#{args})' on #{@object.inspect}")
        @object.send(name, *args, &block)
      end
    end
  end
end

s1 = AuditDecorator.new("ruby!")
p s1.class

PS:话说,这是那本书?

#1 楼 @watraludru EffectiveRuby 中文版 元编程的一节。

才发现你标题用的是 define_method,然后问题问的是 define_singleton_method,两者不是一个概念,不知道你的理解是怎么样的,总感觉这么实现的装饰器怪怪的。书没看过,不知道原书是否有误。

这里出错是因为 name 依次被取到了 :define_signleton_method,:send,:xxxx。取到 :define_singleton_method 后,定义了同名的单件方法,所以你后面出现了一堆日志信息,同时这个定义会导致以后的定义也在 @object 上也定义对应的单件方法。取到 :send 后,@object 上会定义同名(send)的单件方法,然后这个单件方法会使用 @logger@object 变量,所以出现未初始化的实例变量。取到 :xxxx 时,会调用先前你定义的同名 send 的单件方法,然后出现 nil 的问题。从结果上来看,应该是这么一回事。至于第二步为什么会这样,咱一时也不明白,应该跟单件方法的上下文有关。

#3 楼 @watraludru 不好意思,标题是写错了。原文一开始是用 define_method 使用匿名模块实现的。

require 'logger'

class AuditDecorator
  def initialize (object)
    @object = object
    @logger = Logger.new(STDOUT)

    mod = Module.new do
      object.public_methods.each do |name|
        define_method(name) do |*args, &block|

          @logger.info("calling '#{name}' on #{@object.inspect}")
          @object.send(name, *args, &block)
        end
      end
    end

    extend(mod)
  end
end

s1 = AuditDecorator.new("Ruby!")
puts s1.downcase
p s1.class

书中的 define_singleton_method 实现见楼下图片。

GitHub 拉了下代码,跑了下测试用例。用作者指定的版本(2.1.2)是可以运行通过的,但仍有部分日志信息;用最新的版本(2.3.0)就报错。

原因正如前面所说,其中 2.1.2 版本的 public_methods 返回的列表,:send 在前,:define_singleton_method 在后,所以有日志,没报错;而 2.3.0 版本返回的列表,:send 在后,:define_signleton_method 在后,所以就报错了。

个人觉得,原作者用 define_singleton_method 实现的装饰器代码有不够准确。若你有时间,可以邮件问下作者或译者喽,或者直接提个 issue 看下啦。

另外,在这个标题上加个 《Effective Ruby》有误? 如何,弄个大新闻,看下别人有什么看法啥。》。》

irb(main):041:0> require 'logger'
=> false
irb(main):042:0> 
irb(main):043:0* class AuditDecorator
irb(main):044:1>   def initialize (object)
irb(main):045:2>     @object = object
irb(main):046:2>     @logger = Logger.new(STDOUT)
irb(main):047:2> 
irb(main):048:2*     [:define_singleton_method, :send, :object_id].each do |name|
irb(main):049:3*       define_singleton_method(name) do |*args, &block|
irb(main):050:4*         p "define #{name}, current_self #{self}"
irb(main):051:4>         @logger.info("calling '#{name}' on #{@object.inspect}")
irb(main):052:4>         @object.send(name, *args, &block)
irb(main):053:4>       end
irb(main):054:3>     end
irb(main):055:2>   end
irb(main):056:1> end
=> :initialize
irb(main):057:0> 
irb(main):058:0* s1 = AuditDecorator.new("ruby!")
"define define_singleton_method, current_self #<AuditDecorator:0x007f8ef12a1188>"
I, [2016-04-07T22:44:47.403807 #2267]  INFO -- : calling 'define_singleton_method' on "ruby!"
"define define_singleton_method, current_self #<AuditDecorator:0x007f8ef12a1188>"
I, [2016-04-07T22:44:47.403938 #2267]  INFO -- : calling 'define_singleton_method' on "ruby!"
"define send, current_self ruby!"
NoMethodError: undefined method `info' for nil:NilClass
    from (irb):51:in `block (2 levels) in initialize'
    from (irb):52:in `block (2 levels) in initialize'
    from (irb):49:in `block in initialize'
    from (irb):48:in `each'
    from (irb):48:in `initialize'
    from (irb):58:in `new'
    from (irb):58
    from /Users/xiaohui/.rbenv/versions/2.2.3/bin/irb:11:in `<main>'
需要 登录 后方可回复, 如果你还没有账号请 注册新账号