类变量

类方法可能会让你想到类变量(也就是名称以 @@ 开头的变量)。你可能还记得我们之前在一个简单的冒险游戏中使用了类变量(参见第 2 章中的 2adventure.rb)来记录游戏中对象的总数; 每次创建一个新的 Thing 对象时,都会在 @@num_things 类变量中增加 1:

  1. class Thing
  2. @@num_things = 0
  3. def initialize(aName, aDescription)
  4. @@num_things +=1
  5. end
  6. end

与实例变量(在从类派生的对象中)不同,类变量必须在首次声明时给出一个值:

  1. @@classvar = 1000 # class variables must be initialized

在类内初始化实例或类变量只会影响类本身存储的值。类变量(class variable)既可用于类本身,也可用于从该类创建的对象。但是,每个实例变量都是唯一的;每个对象都有属于自己的任何实例变量的副本 - 而类本身也可能有自己的实例变量。

类变量、实例变量以及方法的总结

实例变量以 @ 开头: @myinstvar # instance variable类变量以 @@ 开头: @@myclassvar # class variable实例方法由以下定义:def <MethodName> def anInstanceMethod # some code end类方法则由以下定义:def <ClassName>.<MethodName> def MyClass.aClassMethod # some code end
class_methods2.rb

要了解一个类怎样才会拥有实例变量,请查看 class_methods2.rb 程序。这里声明并初始化了一个类变量和一个实例变量:

  1. @@classvar = 1000
  2. @instvar = 1000

它定义了一个类方法 classMethod,将这两个变量递增 10,还有一个实例方法 instanceMethod,将两个变量递增 1。请注意,我还为实例变量 @instvar 赋了值。我之前说过,初始值通常不会以这种方式分配给实例变量。该规则的例外是将值赋给类本身的实例变量,而不是从该类派生的对象的实例变量。不久之后,这个区别将变得更加明显。

我编写了几行代码来创建 MyClass 类的三个实例(ob 变量在每个循环中初始化一个新实例),然后调用类和实例方法:

  1. for i in 0..2 do
  2. ob = MyClass.new
  3. MyClass.classMethod
  4. ob.instanceMethod
  5. puts( MyClass.showVars )
  6. puts( ob.showVars )
  7. end

我还编写了另一个类方法 MyClass.showVars 和一个实例方法 showVars,以便在每个循环中显示 @instvar@@classvar 的值。当你运行代码时,将会显示的值:

  1. (class method) @instvar = 1010, @@classvar = 1011
  2. (instance method) @instvar = 1, @@classvar = 1011
  3. (class method) @instvar = 1020, @@classvar = 1022
  4. (instance method) @instvar = 1, @@classvar = 1022
  5. (class method) @instvar = 1030, @@classvar = 1033
  6. (instance method) @instvar = 1, @@classvar = 1033

你可能需要仔细查看这些结果才能看到发生了什么。总之,这就是发生的事情:类方法 MyClass.classMethod 和实例方法 instanceMethod 中的代码都在递增类和实例变量,@@classvar@instvar

你可以清楚地看到类变量通过这两种方法都在递增(无论何时创建新对象,类方法都会向 @@classvar 添加 10,实例方法为它添加 1)。但是,每当创建新对象时 instanceMethod 都会将其实例变量初始化为 1。这是预期的行为 - 因为每个对象都有自己的实例变量的副本,但所有对象共享一个唯一的类变量。

也许不太明显的是,类本身也有自己的实例变量 @instvar。这是因为,在 Ruby 中,类也是一个对象,因此可以包含实例变量,就像任何其它对象一样。MyClass 变量 @instvar 由类方法 MyClass.classMethod 递增:

  1. @instvar += 10

注意当实例方法 showVars 打印 @instvar 的值时,它打印存储在特定对象 ob 中的值; ob@instvar 的值最初是 nil(并非像 MyClass 的变量 @instvar 一样初始值为 1000)并且此值在 instanceMethod 中递增 1。

当类方法 MyClass.showVars 打印 @instvar 的值时,它打印存储在类本身中的值(换句话说,MyClass 的 @instvar 与来自 ob@instvar 是不同的变量)。但是当任一方法打印出类变量 @@classvar 的值时,值是一样的。

请记住,只会有一个类变量的副本,但可能会有许多实例变量的副本。如果这仍然令人困惑,请看看 inst_vars.rb 程序:

inst_vars.rb
  1. class MyClass
  2. @@classvar = 1000
  3. @instvar = 1000
  4. def MyClass.classMethod
  5. if @instvar == nil then
  6. @instvar = 10
  7. else
  8. @instvar += 10
  9. end
  10. end
  11. def instanceMethod
  12. if @instvar == nil then
  13. @instvar = 1
  14. else
  15. @instvar += 1
  16. end
  17. end
  18. end
  19. ob = MyClass.new
  20. puts MyClass.instance_variable_get(:@instvar)
  21. puts( '--------------' )
  22. for i in 0..2 do
  23. # MyClass.classMethod
  24. ob.instanceMethod
  25. puts("MyClass @instvar=#{MyClass.instance_variable_get(:@instvar)}")
  26. puts("ob @instvar= #{ob.instance_variable_get(:@instvar)}")
  27. end

这一次,我们在一开始就创建了一个实例(ob),而不是通过循环每次创建一个新的对象实例。当 ob.instanceMethod 调用时,@instvar 增加 1。

在这里,在类和方法中我使用了一个小技巧,使用 Ruby 的 instance_get_variable 方法获取 @instvar 的值:

  1. puts("MyClass @instvar=#{MyClass.instance_variable_get(:@instvar)}")
  2. puts("ob @instvar= #{ob.instance_variable_get(:@instvar)}")

因为我们只增加属于对象 ob@instvar,所以当 for 循环执行时,@instvar 的值从 1 上升到 3。但是属于 MyClass 类的 @instvar 永远不会增加; 它保持在初始值(1000)…

  1. 1000
  2. --------------
  3. MyClass @instvar= 1000
  4. ob @instvar= 1
  5. MyClass @instvar= 1000
  6. ob @instvar= 2
  7. MyClass @instvar= 1000
  8. ob @instvar= 3

但现在,取消掉这一行的注释…

  1. MyClass.classMethod

现在调用一个类方法,它将 @instvar 增加 10。这次当你运行程序时,你会看到,像以前一样,ob@instvar 变量在每次循环中增加 1 而 MyClass 的 @instvar 变量则会增加 10 …

  1. 1000
  2. --------------
  3. MyClass @instvar= 1010
  4. ob @instvar= 1
  5. MyClass @instvar= 1020
  6. ob @instvar= 2
  7. MyClass @instvar= 1030
  8. ob @instvar= 3

一个类是一个对象

要理解这一点,只需记住一个类是一个对象(实际上,它是 Class 类的一个实例!)。MyClass ‘类对象’(class object)有自己的实例变量(@instvar),就像 ob 对象有自己的实例变量(在这里,也恰好称为 @instvar)。实例变量对于对象实例始终是唯一的 - 因此没有两个对象(甚至像 MyClass 这样恰好是一个类的对象!)可以共享一个实例变量。