Rails: переменная cattr_accessor и класса - PullRequest
3 голосов
/ 16 июля 2010

Выполнение этого кода:

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

Разрывы так:

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

Если мы поменяем местами последние два блока кода (так, чтобы b.set_my_name_attr вызывался раньше b.set_my_name_var)все работает нормально.

Похоже, что @@my_name рассматривается как переменная класса модуля A, а не класса B (как я и ожидал).Разве это не смущает?Где можно узнать больше о переменных класса модуля?

1 Ответ

3 голосов
/ 16 июля 2010

Если у вас есть set_my_name_var метод в модуле A, выполняющий @@my_name = 'A', это устанавливает переменную модуля в A. Это поведение не меняется, когда метод вызывается через включающий класс. Это также приводит к другому факту, который иногда привлекает внимание людей - если вы включаете 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"

Обновление

Думаю, я понял, что происходит, и это объяснит, почему это не работает так, как вы ожидаете. 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...

Происходит следующая последовательность:

  • B определяется
  • модуль A включен
  • обратный вызов included делает klass.send(:cattr_accessor, :my_name).
  • и @@my_name создается в классе B, для которого установлено nil.

Без cattr_accessor, тогда после вызова set_my_name_var, когда вы говорите @@my_name в B, это будет означать переменную модуля. Но с помощью cattr_accessor в классе теперь существует переменная с тем же именем, поэтому, если мы скажем @@my_name в B, мы получим значение переменной B вместо A. Это то, что я имел в виду под маскировкой. (Переменная B мешает нам видеть A)

Может быть, следующее проиллюстрирует. Представьте, что мы дошли до вашего b = B.new, и мы делаем следующее:

>> 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

Я думаю, cattr_reader делает это, чтобы избежать ошибок uninitialized class variable, если вы пытаетесь использовать метод получения перед установщиком. (переменные класса по умолчанию не равны nil так же, как переменные экземпляра.)

...