如何让attr_accessor_with_default与集合一起工作?

时间:2010-08-14 00:10:51

标签: ruby-on-rails ruby-on-rails-3

我想给我的一个模型一个属性访问器,默认为八个零的数组。这是我尝试的第一个语法:

attr_accessor_with_default:weekly_magnitude_list, [0,0,0,0,0,0,0,0]

上面没有达到我的预期,因为模型的所有实例最终共享同一个Array对象。引用我的博客(http://barelyenough.org/blog/2007/09/things-to-be-suspicious-of-attr_accessor_with_default-with-a-collection/)提出了不同的语法,基本上将默认值包装在一个块中。

attr_accessor_with_default(:weekly_magnitude_list) {[0,0,0,0,0,0,0,0]}

这不起作用(对我来说,在Rails 3中)。每当我调用访问器时,我似乎都会得到一个全新的Array对象。这实际上意味着我无法写信。

有人知道这样做的正确方法吗?

为了您的荣幸,我已经包含了一个简单测试的输出,证明了这一点:

class Container
  attr_accessor_with_default :naive_collection, [0,0]
  attr_accessor_with_default(:block_collection) {[0,0]}
end   
> c = Container.new
=> #<Container:0x7f3610f717a8>
> c.naive_collection[0] = "foo"  
=> "foo"   
> Container.new.naive_collection
=> ["foo", 0]
# expected [0,0]

> c.block_collection[0] = "foo"
=> "foo"
> c.block_collection
=> [0, 0]
# expected ["foo", 0]

1 个答案:

答案 0 :(得分:1)

我在遇到同样的问题时偶然发现了这个问题。

作为参考,docs指定在实例范围内动态评估块形式。继续这个例子,它的用处非常有限,但至少按照你期望的方式工作:

class Container
  attr_accessor_with_default(:block_collection) { name.underscore }
end

> c = Container.new(:name => "TestName")
> c.block_collection  # => "test_name"
> c.block_collection = "something else"  # => "something else"
> c.name => "TestName"

这是真正古怪的部分,但是......

class Container
  attr_accessor_with_default :naive_collection, [0, 0]
end

# This works as expected
> c = Container.new
> c.naive_collection = ["foo", "bar"]  # => ["foo", "bar"]
> Container.new.naive_collection  # => [0, 0]
> c.naive_collection[0] = 0  # => [0, "bar"]
> Container.new.naive_collection  # => [0, 0]

# But this doesn't
> c2 = Container.new
> c2.naive_collection  # => [0, 0]
> c2.naive_collection[0] = "problem!"  # => ["problem!", 0]
> Container.new.naive_collection  # => ["problem!", 0]

深入挖掘源代码,我看到attr_accessor_with_default定义了一个返回默认值或执行块的实例方法。哪个没问题。

然后继续在module_eval中执行它:

def #{sym}=(value)
  class << self;
    attr_reader :#{sym}
  end

  @#{sym} = value
end

这简直令人费解。如果我无法弄清楚这种行为的动机,我最终可能会将此作为一个错误。

<强>更新

我设法弄清楚这里出了什么问题。

def attr_accessor_with_default(sym, default = Proc.new)
  define_method(sym, block_given? ? default : Proc.new { default })
  module_eval(<<-EVAL, __FILE__, __LINE__ + 1)
    def #{sym}=(value)
      class << self; attr_accessor :#{sym} end
      @#{sym} = value
    end
  EVAL
end

最初,默认值作为proc存在。 一旦调用setter,getter和setter方法将被attr_accessor方法覆盖,并初始化实例变量。 问题是getter中的默认proc返回类级别的默认值。所以,当你做类似的事情时:

> c2.naive_collection[0] = "problem!"  # => ["problem!", 0]

您实际上正在更改班级的默认值。

我认为这种方法可能应该实现为:

class Module
  def attr_accessor_with_default(sym, default = Proc.new)
    module_eval(<<-EVAL, __FILE__, __LINE__ + 1)
      def #{sym}
        class << self; attr_reader :#{sym} end
        @#{sym} = #{ default.dup }
      end

      def #{sym}=(value)
        class << self; attr_accessor :#{sym} end
        @#{sym} = value
      end
    EVAL
  end
end

我会打票并提供补丁。

再次更新 https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/6496