论坛首页 编程语言技术论坛

慎用类变量 - 实例变量靠谱量又足

浏览 13977 次
该帖已经被评为良好帖
作者 正文
   发表时间:2007-11-04  
请各位看官先阅读potian大牛图文并茂的经典贴: Ruby 单件类
http://www.iteye.com/topic/20352

既然类也是Object,也可以有实例变量(instance variable),则在类上的实例变量跟类变量(class variable)不就一样了?

举例说明 类上的实例变量 :
tip1: ruby对外暴露的是只能是方法,不能是变量。
tip2: class variable是指在类级别上这样声明的变量 @@abc = 123

class A
  @x = 1
  def initialize
    @y = 2
  end
end

a = A.new

puts A.instance_variable_get(:@x)     # 1
puts A.instance_variable_get(:@y)     # nil
puts a.instance_variable_get(:@x)     # nil
puts a.instance_variable_get(:@y)     # 2


看来,在什么上下文对象上执行@xx赋值,就是给它个对象设置实例变量. 
对于上例, @x是属于A这个常量(独一份),而非每个A实例有自己单独的一个.
从概念上, A的@x不就跟类变量(class variable)一样了?

有人做了实验:
Ruby Class Variables, Attributes and Constants
http://ghouston.blogspot.com/2006/06/ruby-class-variables-attributes-and.html

还有人因此好心提醒各位市民:
Don't Use Class Variables!
http://www.oreillynet.com/ruby/blog/2007/01/nubygems_dont_use_class_variab_1.html

从上面两个链接分析来看,一个object上的methods是让它的类、父类、mixin的module等来保持,当调用一个方法时是通过在其类/父类/module上搜索的机制来确定这个方法的具体位置。
这更接近java等语言,同名方法可以分布在不同层次,子类优先。事实上,java的子类和父类之间,也可以存在同名的成员变量,也用类似的查找路径机制。(这种情况下会不会引起潜在的bug是另个话题了)
但ruby object上的variables就没那么好运了,事实上,ruby object并不在类上定义实例变量,而是执行过程中遇到@xx=value这样的赋值语句时才会给它绑上,当遇到读取@xx时如果定义过则返回其值否则返回nil,即,实例变量是object本身保持的,而非它的类/父类(可以想像成每个object自己维持一个Hash),自然就没有了查找路径这一说。(java在类上声明成员变量,更像是用类定义了一个数据模板,而ruby没有数据模板这个概念)

ruby1.8的类变量(class variable)是父类子类引用同一个东东,所以才有上面好心人提醒。这个实现细节确实大大出乎使用者的预料。
类(A)本身就属于一个const,只有一份,所以类变量也只有一份,那跟gloal variable还有什么区别? 试着解释一下:
1. "权当A是全局变量的namespace了"。 解释不通,那不如提供一个简单的类/DSL封装好看。
2. "A和a都可以访问类变量"。 确实,相比 实例变量 是优势。再次注意访问实例变量只从当前object上找。因此A上面的@x, a不能直接访问到,除非用a.class.instance_variable_get。
  但怎么说子类父类引用同一个东东称之为class variable太诡异了。

对于这个不靠谱的实现,ruby1.9做了调整,但似乎还没百分百确定下来:
(Changes in Ruby 1.9) Class variables are not inherited
http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9#l38

对于const,跟variable不太一样的是它有查找路径(命名空间等)的问题,上面链接的实验中有些结论,但事情总在变化当中,不能太依赖它的实现细节,那些语言的设计师们似乎也没想太明白:
(Ruby Conference 2005) Wild and Weird Ideas:
http://www.rubyist.net/~matz/slides/rc2005/mgp00015.html


如果是类变量概念的使用场景,想让A和a都能便利的直接访问,又不想受到类变量实现缺陷的困扰,建议用active_support(rails组成部份之一)提供的class_inheritable_accessor,它本质是用A上的实例变量(instance variable)保持数据,在子类继承时复制一份新的,并在A和a上生成方法以方便外界调用/存取。(不禁感叹,只有实例变量是靠谱且够用的,我们一直用它!)

与此相对的,是cattr_accessor,它是用类变量(class variable)保持数据,仅生成了方法方便外界存取,因此当类上有继承关系时,需十分注意。
使用方法很简单:
class A
  class_inheritable_accessor :vv
end
a = A.new
A.vv = 123
puts a.vv


这两个文件分别在
引用

gems/activesupport-x.x.x/lib/active_support/core_ext/class/
attribute_accessors.rb
inheritable_attributes.rb


----
potian大牛mixin的文章也没着落了,难道被邪恶滴Erlang勾走了?翘首盼望偶像有空回来看看俺们这些忠诚的粉丝~
   发表时间:2007-11-24  
这几天被这个class_inheritable_accessor困扰了好久(主要遇到的是类继承问题,类和实例公用变量没有问题),发现class_inheritable_accessor似乎会有个“富不过三代”问题,最后在项目里不断抽丝剥茧终于简化出如下演示代码(直接黏贴到rails console观看):
class Base
  class_inheritable_accessor :var
  class << self
	  def set_var(new_var)
	    self.var ||= [{:a=>"aa"}, {:b=>"bb"}]
	    self.var.each do |v|
	      v[:a] = new_var
	    end
	  end
  end
end

class A < Base
 # 再声明一次也没用
 class_inheritable_accessor :var
 set_var "a"
end

class B < Base
end

class C < A
end

A.var
#=> [{:a=>"a"}, {:b=>"bb", :a=>"a"}]

# B和A是兄弟,没有干扰 
B.set_var('b')
#=> [{:a=>"b"}, {:b=>"bb", :a=>"b"}]

A.var
#=> [{:a=>"a"}, {:b=>"bb", :a=>"a"}]

# C是A的子类,但这时已经没有“代沟”了 
C.set_var('c')
#=> [{:a=>"c"}, {:b=>"bb", :a=>"c"}]

# *notice*
A.var
#=> [{:a=>"c"}, {:b=>"bb", :a=>"c"}]



必须注意var的值为数组且包含HASH时才会触发这个问题(开始用简单字符值来测试时怎么也无法重现项目中出现的问题,郁闷至极),原因估计是如楼主所说的“class_inheritable_accessor,它本质是用A上的实例变量(instance variable)保持数据,在子类继承时复制一份新的”(不过我看实现代码似乎不是用实例变量,而是类变量),但Rails的copy似乎不是deep clone,所以才有这个“陷阱”。

0 请登录后投票
   发表时间:2007-11-24  
补充示范,var为非HASH数组时,ABC各自独立:

class Base
  class_inheritable_accessor :var
  class << self
	  def set_var(new_var)
	    self.var ||= {:a=>"aa"}
	    self.var[:a] = new_var
	  end
  end
end

class A < Base
 class_inheritable_accessor :var

 set_var "a"
end

class B < Base
end

class C < A
end

A.var #=> {:a=>"a"}
B.set_var('b')
A.var #=> {:a=>"a"}
C.var #=> {:a=>"a"}
C.set_var('c')
A.var #=> {:a=>"a"}




另一个触发条件是,只要在class中调用了set_var,那么它的子类都会共用父类的var变量,
示范例子,当A里不调用set_var,不管数组多深,子孙有多少,A都是原来的A:


class Base
  class_inheritable_accessor :var
  class << self
      def set_var(new_var)
        self.var ||= [{:a=>"aa"}, {:b=>"bb"}]
        self.var.each do |v|
          v[:a] = new_var
        end
      end
  end
end

class A < Base
end

class B < Base
end

class C < A
end

A.var #=> nil
B.set_var('b')
A.var #=> nil
C.set_var('c')
A.var #=> nil


0 请登录后投票
   发表时间:2007-11-24  
(回复了一个错误的信息,我删除了,请原谅)
0 请登录后投票
   发表时间:2007-11-24  
类变量(@@xx)的问题在于它本质跟全局变量一样.
0 请登录后投票
   发表时间:2007-11-24  
不对,我理解错了。抱歉。项目详细的就不说了
0 请登录后投票
   发表时间:2007-11-24  
汗,迟了一步。
看看错误的理解也好嘛,那后人就可以避免犯同样错误:)

liusong1111 楼主对我遇到的问题有没什么看法?果真是ROR的BUG还是我的BUG?
0 请登录后投票
   发表时间:2007-11-24  
谢谢rainchen指出class_inheritable_accessor的问题,查看inheritable_attributes.rb源码果然用了value.dup,也不是deepcopy(真正靠谱的实现deepcopy也不容易,the ruby way说用Marshal dump load一遍,似乎内置有deepcopy的是python不是ruby),而且如果dup后 父类属性又有改变会怎样? 使用这些高档货如果不深入实现细节心里还是没底. 确认没有用到类变量(@@)
0 请登录后投票
   发表时间:2007-11-24  
liusong1111 写道
谢谢rainchen指出class_inheritable_accessor的问题,查看inheritable_attributes.rb源码果然用了value.dup,也不是deepcopy(真正靠谱的实现deepcopy也不容易,the ruby way说用Marshal dump load一遍,似乎内置有deepcopy的是python不是ruby),而且如果dup后 父类属性又有改变会怎样? 使用这些高档货如果不深入实现细节心里还是没底. 确认没有用到类变量(@@)


哦,看来是术语理解不同,我理解的“类变量”是
class A   
  @x = 1   
  def initialize   
    @y = 2   
  end  
end 

中的@x
这是相对于@y这个类的实例变量来说的(似乎你的描述是认为@x是实例变量)

翻查了下“课本”,的确@@这种才叫类变量(Class Variables),那么@x的正式称呼是啥?
因为class_inheritable_accessor实际是用了@inheritable_attributes这个“作用域在类上的实例变量(?)”


其实关于深层拷贝这个BUG,似乎在一年前就被人指出并修补好了,难道是因为触发条件的问题?
0 请登录后投票
   发表时间:2007-11-24  
嗯, 是术语没统一. 这个“作用域在类上的变量”实际上也是实例变量,不知道怎么叫好,我上面的贴写的也很拗口啊.
既有深度克隆的问题,也有执行先后顺序的问题,着实难办。对于class inheritable attribute为Array或Hash的情况,rails还提供了class_inheritable_array,class_inheritable_hash,也不过是让子类能在父类的容器元素不丢失也不受影响的前提下追加元素:
  def write_inheritable_array(key, elements)
    write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
    write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
  end

0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics