深入探索
超类
要理解 super 关键字的工作原理,请看我们的示例程序,super.rb。它包含五个相关类:Thing 类其他所有类的祖先;从 Thing 类衍生到 Thing2 类,再从 Thing2 类衍生到 Thing3 类,然后依次是 Thing4 和 Thing5 类。
让我们仔细看看这个层次中的前三个类:Thing 类有两个实例变量,@name
和 @description
;Thing2 类也定义了 @fulldescription
(一个包含 @name
与 @description
的字符串);Thing3 类则添加了另一个变量 @value
。
这三个类各自包含一个 initialize
方法,用来在创建新对象时设置变量的值;它们各自也有一个名为 aMethod
的方法,用来改变一个或多个变量的值。后代类 Thing2 和 Thing3 在它们的方法中都使用了 super
关键字。
在这段代码的底部,我写了一个 “main” 循环,这将在你运行该程序时执行。不要担心不懂这些语法,我们将在以后的章节中学习循环。我已经将 test1
到 test5
添加进了循环,所以你可以很容易地运行包含在不同方法中的代码。首次运行此程序时,在提示符处键入数字 1
然后按 Enter 键。 这将运行包含这两行代码的test1方法:
t = Thing.new( "A Thing", "a lovely thing full of thinginess" )
t.aMethod( "A New Thing" )
第一行在这里创建并初始化 Thing 对象,第二行调用它的 aMethod
方法。因为 Thing 类并非派生自其它类(事实上,所有的 Ruby 类都派生自 Object 类,它是其它所有类的祖先类),所以在这里并没有其它的事情发生。当调用 Thing.initialize
和 Thing.aMethod
方法时,输出调用了 inspect
方法来显示对象的内部结构。inspect
方法可以被所有对象调用,是个调试的工具方法。在这里,它给我们显示一个十六进制的数字,来表示这个拥有 @name
和 @description
字符串变量的特定对象。
现在,在提示符下,输入 2
来运行 test2
包含的代码,创建一个 Thing2 对象 t2
并调用 t2.aMethod
方法:
t2 = Thing2.new( "A Thing2", "a Thing2 thing of great beauty" )
t2.aMethod( "A New Thing2", "a new Thing2 description" )
仔细看看输出,你会看到即使 t2
是一个 Thing2 对象,首先被调用的却是 Thing 类的 initialize
方法。想理解为什么会这样,看看 Thing2 类的 initialize
方法的代码:
def initialize(aName, aDescription)
super
@fulldescription = "This is #{@name}, which is #{@description}"
puts("Thing2.initialize: #{self.inspect}\n\n")
end
这里使用了 super
关键字调用了 Thing2 类的祖先类或者说”超类” 的 initialize
方法。你可以从声明中看到 Thing2 类的超类是 Thing 类:
class Thing2 < Thing
在 Ruby 中, 当 super
关键字本身单独使用时(即不带任何参数),它将会把当前方法(这里是 Thing2.initialize
)所有参数传递给在其超类中的同名方法(这里是 Thing.initialize
)。或者,你可以显式的指定 super
后的参数列表。因此,在本例中以下代码将具有相同的效果:
super(aName, aDescription)
虽然允许单独使用 super
关键字,但是通常为了清楚起见,应该明确指定要传递给超类方法的参数列表。无论如何,如果你想要传递有限的参数,则需要一个明确的参数列表。例如,Thing2 的 aMethod
方法仅将 Name
参数传递给其超类 Thing1 的 initialize
方法:
这解释了为什么 @description
变量在 Thing2.aMethod
方法被调用时为何不会改变。
现在,你看看 Thing3 类,你会看到这增加了一个更多的变量,@value
。initialize
方法传递了两个参数 aName
和 aDescription
给它的超类 Thing2。反过来,正如我们所看到的,Thing2 类的 initialize
同样地将这些参数传递给了它的超类 Thing 的 initialize
方法。
程序运行时,在提示符下输入 3
以查看输出。这是此次执行的代码:
t3 = Thing3.new("A Thing 3", "a Thing3 full of Thing and Thing2iness",500)
t3.aMethod( "A New Thing3", "and a new Thing3 description",1000)
注意执行流程是如何在类层次结构中向上传递的,在 Thing 中的 initialize
和 aMethod
方法在执行前匹配 Thing2 和 Thing3 中的同名方法。
到目前为止,我做的例程中它们并没有强制重写超类的方法。这只有当你想添加一些新行为的时候才会需要。Thing4 省略了 initialize
方法,但实现了 aMethod
方法。
输入 4
执行下面的代码:
t4 = Thing4.new( "A Thing4", "the nicest Thing4 you will ever see", 10 )
t4.aMethod
当你运行它,发现第一个可用的 initialize
被调用时, Thing4 对象被创建。这恰好是 Thing3.initialize
,同时也会再次调用其祖先类 Thing2 和 Thing 的 initialize
方法。然而,Thing4 实现的 aMethod
方法没有调用它的超类方法,因此其它祖先类中的方法会被忽略,这里将立即执行。
最后,Thing5 继承自 Thing4,不会引入任何新的数据或方法。在提示符处输入 5
以执行以下代码:
t5 = Thing5.new("A Thing5", "a very simple Thing5", 40)
t5.aMethod
这次你会看到,调用 new
方法会导致 Ruby 回溯类层次结构,直到找到第一个 initialize
方法。而这存在于 Thing3 中(同样也是 Thing2 和 Thing3 的初始化方法)。然而,aMethod
方法的第一个实现在 Thing4 中,它没有调用 super
,所以从这里就结束了。
所有的 Ruby 类都是继承自 Object 类。
Object 类本身没有超类,任何尝试获取它的超类行为都会返回 nil 。
begin x = x.superclass puts(x) end until x == nil类中的常量
有时可能需要访问在类中声明的常量(以大写开头)。让我们假设你有下面这个类:
class X
A = 10
class Y
end
end
为了访问常量 A
,你需要使用特殊范围运算符 ::
,像这样:
X::A
类名是常量,所以这个操作符允许你访问其他类中的类,这使得可以从“嵌套”类创建对象,如类 X
中的类 Y
:
ob = X::Y.new
局部类
在 Ruby 中,定义一个类在同一个地方是不必要的,如果你愿意,你可以在程序的不同地方定义同一个类。当一个类派生自特定的超类,每个后续的类定义部分使用 <
操作符指定超类是可选的。
这里我创建了两个类,A 以及 派生自 A 的 B:
class A
def a
puts( "a" )
end
end
class B < A
def ba1
puts( "ba1" )
end
end
class A
def b
puts( "b" )
end
end
class B < A
def ba2
puts( "ba2" )
end
end
现在,如果我创建一个 B 对象,A 和 B 的所有方法都是可用的:
ob = B.new
ob.a
ob.b
ob.ba1
ob.ba2
你还可以将功能添加到 Ruby 的标准类中,例如 Array:
class Array
def gribbit
puts( "gribbit" )
end
end
这会将 gribbit
方法添加到 Array 类中,以便现在可以执行以下代码:
[1,2,3].gribbit