在Ruby中通过instance_eval调用块时,动态添加的访问器分配不起作用

时间:2010-12-09 22:58:35

标签: ruby dynamic dsl instance-eval

我有一个类,我在运行时动态添加属性访问器。此类构成DSL的一部分,从而将块传递给配置方法并使用instance_eval调用。这使得DSL在引用类的方法时可以删除对“self”的引用。

但是,我发现我可以引用属性来检索它们的值,但我无法分配它们,除非明确引用self,如下面的代码示例所示。

class Bar

  def add_dynamic_attribute_to_class(name)
    Bar.add_dynamic_attribute(name)
  end

  def invoke_block(&block)
    instance_eval &block
  end

  def self.add_dynamic_attribute(name)
    attr_accessor name
  end

end

b = Bar.new

b.add_dynamic_attribute_to_class 'dyn_attr'

b.dyn_attr = 'Hello World!'

# dyn_attr behaves like a local variable in this case
b.invoke_block do
  dyn_attr = 'Goodbye!'
end

# unchanged!
puts "#{b.dyn_attr} but should be 'Goodbye!'"

# works if explicitly reference self
b.invoke_block do
  self.dyn_attr = 'Goodbye!'
end

# changed...
puts "#{b.dyn_attr} = 'Goodbye!"

# using send works
b.invoke_block do
  send 'dyn_attr=', 'Hello Again'
end

# changed...
puts "#{b.dyn_attr} = 'Hello Again!"

# explain this... local variable or instance method?
b.invoke_block do

  puts "Retrieving... '#{dyn_attr}'"

  # doesn't fail... but no effect
  dyn_attr = 'Cheers'

end

# unchanged
puts "#{b.dyn_attr} should be 'Cheers'"

任何人都可以解释为什么这不符合预期吗?

1 个答案:

答案 0 :(得分:4)

问题在于Ruby处理实例和局部变量的方式。发生的事情是你在instance_eval块中设置一个局部变量,而不是使用ruby访问器。

这可能有助于解释它:

class Foo
  attr_accessor :bar

  def input_local
    bar = "local"
    [bar, self.bar, @bar, bar()]
  end

  def input_instance
    self.bar = "instance"
    [bar, self.bar, @bar, bar()]
  end

  def input_both
    bar = "local"
    self.bar = "instance"
    [bar, self.bar, @bar, bar()]
  end
end

foo = Foo.new
foo.input_local #["local", nil, nil, nil]
foo.input_instance #["instance", "instance", "instance", "instance"]
foo.input_both #["local", "instance", "instance", "instance"]

bocks的工作方式是区分本地变量和实例变量,但是如果在调用reader时没有定义局部变量,则类默认为实例变量(就像在我的中调用input_instance一样)例子)。

有三种方法可以获得您想要的行为。

使用实例变量:

    class Foo
      attr_accessor :bar

      def evaluate(&block)
        instance_eval &block
      end
    end

    foo = Foo.new
    foo.evaluate do
      @bar = "instance"
    end
    foo.bar #"instance"

使用自变量:

    class Foo
      attr_accessor :bar

      def evaluate(&block)
        block.call(self)
      end
    end

    foo = Foo.new
    foo.evaluate do |c|
      c.bar = "instance"
    end
    foo.bar #"instance"

使用setter函数:

    class Foo
      attr_reader :bar
      def set_bar value
        @bar = value
      end

      def evaluate(&block)
        instance_eval &block
      end
    end

    foo = Foo.new
    foo.evaluate do
      set_bar "instance"
    end
    foo.bar #"instance"

所有这些示例都将foo.bar设置为“instance”。