Rails: cattr_accessor和类变量

时间:2022-03-19 12:38:38

Running this code:

运行这段代码:

module A
  def self.included(klass)
    klass.send(:cattr_accessor, :my_name)
  end

  def set_my_name_var
    @@my_name = 'A' # does NOT work as expected
  end

  def set_my_name_attr
    self.class.my_name = 'A' # works as expected
  end
end

class B
  include A

  cattr_accessor :my_other_name

  def set_my_other_name_var
    @@my_other_name = 'B' # works
  end

  def set_my_other_name_attr
    self.class.my_other_name = 'B' # works
  end
end

b = B.new

b.set_my_other_name_var
puts "My other name is " + B.my_other_name
b.set_my_name_var
puts "My name is " + B.my_name

b.set_my_other_name_attr
puts "My other name is " + B.my_other_name
b.set_my_name_attr
puts "My name is " + B.my_name

Breaks like this:

优惠如下:

My other name is B
TypeError: (eval):34:in `+': can't convert nil into String

If we swap last two blocks of code (so that b.set_my_name_attr gets called before b.set_my_name_var), everything is works fine.

如果我们交换最后两个代码块(b)set_my_name_attr在b.set_my_name_var)之前被调用,一切正常。

It looks like it treats @@my_name as class variable of module A, not class B (as I would expect it to). Isn't it confusing? Where can read more about module class variables?

看起来它将@my_name作为模块A的类变量,而不是类B(正如我所期望的那样)。不是它困惑?在哪里可以阅读更多关于模块类变量的内容?

1 个解决方案

#1


3  

When you have your set_my_name_var method in module A doing @@my_name = 'A' this is setting a module variable in A. This behaviour doesn't change when the method is called via an including class. This also leads to another fact that sometimes catches people out - if you were to include A in multiple classes there is only one instance of @@my_name, not one instance per including class. The following example illustrates this:

当在模块A中有set_my_name_var方法时,这是在A中设置一个模块变量。这也导致了另一个事实,有时会让人感到意外——如果您在多个类中包含一个,那么@@my_name只有一个实例,而不是每个包含类的实例。下面的例子说明了这一点:

module Example
  def name=(name)
    @@name = name
  end

  def name
    @@name
  end
end

class First
  include Example
end

class Second
  include Example
end

irb(main):066:0> f = First.new
=> #<First:0x2d4b80c>
irb(main):067:0> s = Second.new
=> #<Second:0x2d491d8>
irb(main):068:0> f.name = 'Set via f'
=> "Set via f"
irb(main):069:0> s.name
=> "Set via f"

Update

更新

I think I have figured out what is happening that will explain why it doesn't seem to work the way you expect. cattr_reader (and by extension cattr_accessor) contains the following:

我想我已经弄明白了正在发生的事情,这就可以解释为什么事情并不是你所期望的那样。cattr_reader(扩展名cattr_accessor)包含以下内容:

class_eval(<<-EOS, __FILE__, __LINE__)
  unless defined? @@#{sym}  # unless defined? @@hair_colors
    @@#{sym} = nil          #   @@hair_colors = nil
  end

  # code to define reader method follows...

The following sequence takes place:

以下顺序发生:

  • B is defined
  • B是定义
  • module A is included
  • 模块包含一个
  • the included callback does klass.send(:cattr_accessor, :my_name).
  • 所包含的回调是klass。发送(:cattr_accessor:my_name)。
  • an @@my_name is created in class B that is set to nil.
  • 在类B中创建一个@ @@my_name,并将其设置为nil。

Without the cattr_accessor then after calling set_my_name_var when you say @@my_name within B it would refer to the module's variable. But with the cattr_accessor in place a variable with the same name now exists in the class so if we say @@my_name within B we get the value of B's variable in preference to A's. This is what I meant by masking. (B's variable has got in the way of us seeing A's)

如果没有cattr_accessor,那么在调用set_my_name_var之后,当你在B中输入@@my_name时,它将引用模块的变量。但是有了cattr_accessor,类中就存在一个同名的变量,所以如果我们在B中输入@@my_name,我们会得到B的变量的值优先于a。这就是我说的掩蔽。(B的变量阻碍了我们看到A)

Maybe the following will illustrate. Imagine we'd just got as far as your b = B.new and we do the following:

也许下面的例子可以说明这一点。假设我们得到了b = b。新,我们做以下工作:

>> A.class_variables
=> [] # No methods called on A yet so no module variables initialised
>> B.class_variables
=> ["@@my_other_name", "@@my_name"] # these exist and both set to nil by cattr_accessor
>> B.send(:class_variable_get, '@@my_name')
=> nil # B's @@my_name is set to nil
>> b.set_my_name_var # we call set_my_name_var as you did in the question
=> "A"
>> A.send(:class_variable_get, '@@my_name')
=> "A" # the variable in the module is to to 'A' as you expect
>> B.send(:class_variable_get, '@@my_name')
=> nil # but the variable in the class is set to nil
>> B.my_name
=> nil # B.my_name accessor has returned the variable from the class i.e. nil

I think cattr_reader does this to avoid uninitialized class variable errors if you try to use the getter before the setter. (class variables don't default to nil in the same way that instance variables do.)

我认为cattr_reader这样做是为了避免在setter之前使用getter方法来避免未初始化的类变量错误。(类变量不像实例变量那样默认为nil。)

#1


3  

When you have your set_my_name_var method in module A doing @@my_name = 'A' this is setting a module variable in A. This behaviour doesn't change when the method is called via an including class. This also leads to another fact that sometimes catches people out - if you were to include A in multiple classes there is only one instance of @@my_name, not one instance per including class. The following example illustrates this:

当在模块A中有set_my_name_var方法时,这是在A中设置一个模块变量。这也导致了另一个事实,有时会让人感到意外——如果您在多个类中包含一个,那么@@my_name只有一个实例,而不是每个包含类的实例。下面的例子说明了这一点:

module Example
  def name=(name)
    @@name = name
  end

  def name
    @@name
  end
end

class First
  include Example
end

class Second
  include Example
end

irb(main):066:0> f = First.new
=> #<First:0x2d4b80c>
irb(main):067:0> s = Second.new
=> #<Second:0x2d491d8>
irb(main):068:0> f.name = 'Set via f'
=> "Set via f"
irb(main):069:0> s.name
=> "Set via f"

Update

更新

I think I have figured out what is happening that will explain why it doesn't seem to work the way you expect. cattr_reader (and by extension cattr_accessor) contains the following:

我想我已经弄明白了正在发生的事情,这就可以解释为什么事情并不是你所期望的那样。cattr_reader(扩展名cattr_accessor)包含以下内容:

class_eval(<<-EOS, __FILE__, __LINE__)
  unless defined? @@#{sym}  # unless defined? @@hair_colors
    @@#{sym} = nil          #   @@hair_colors = nil
  end

  # code to define reader method follows...

The following sequence takes place:

以下顺序发生:

  • B is defined
  • B是定义
  • module A is included
  • 模块包含一个
  • the included callback does klass.send(:cattr_accessor, :my_name).
  • 所包含的回调是klass。发送(:cattr_accessor:my_name)。
  • an @@my_name is created in class B that is set to nil.
  • 在类B中创建一个@ @@my_name,并将其设置为nil。

Without the cattr_accessor then after calling set_my_name_var when you say @@my_name within B it would refer to the module's variable. But with the cattr_accessor in place a variable with the same name now exists in the class so if we say @@my_name within B we get the value of B's variable in preference to A's. This is what I meant by masking. (B's variable has got in the way of us seeing A's)

如果没有cattr_accessor,那么在调用set_my_name_var之后,当你在B中输入@@my_name时,它将引用模块的变量。但是有了cattr_accessor,类中就存在一个同名的变量,所以如果我们在B中输入@@my_name,我们会得到B的变量的值优先于a。这就是我说的掩蔽。(B的变量阻碍了我们看到A)

Maybe the following will illustrate. Imagine we'd just got as far as your b = B.new and we do the following:

也许下面的例子可以说明这一点。假设我们得到了b = b。新,我们做以下工作:

>> A.class_variables
=> [] # No methods called on A yet so no module variables initialised
>> B.class_variables
=> ["@@my_other_name", "@@my_name"] # these exist and both set to nil by cattr_accessor
>> B.send(:class_variable_get, '@@my_name')
=> nil # B's @@my_name is set to nil
>> b.set_my_name_var # we call set_my_name_var as you did in the question
=> "A"
>> A.send(:class_variable_get, '@@my_name')
=> "A" # the variable in the module is to to 'A' as you expect
>> B.send(:class_variable_get, '@@my_name')
=> nil # but the variable in the class is set to nil
>> B.my_name
=> nil # B.my_name accessor has returned the variable from the class i.e. nil

I think cattr_reader does this to avoid uninitialized class variable errors if you try to use the getter before the setter. (class variables don't default to nil in the same way that instance variables do.)

我认为cattr_reader这样做是为了避免在setter之前使用getter方法来避免未初始化的类变量错误。(类变量不像实例变量那样默认为nil。)