3. 1adventure.rb
让我们来看看,如何在 Ruby 中创建一个派生类。加载 1adventure.rb 程序。该程序以一个
Thing 类的定义开始,该类有两个实例变量@name 和@description。这些变量都是当新
对象创建时在 initialize 方法中赋值的。
实例变量通常不能(也不应该)从类外部直接访问,这取决于封装的原则,这将在最后的课程
中解释。为了能获取每个变量的值,我们需要一个 get 访问方法如 get_name;为了能给变
量赋值,我们需要一个 set 访问方法如 set_name。
超类和子类
查看 Treasure 类,注意其声明方式:
class Treasure < Thing
这个尖括号'<'指明 Treasure 类是 Thing 的一个子类或者派生类,从 Thing 类中继承数据
(variables/变量)和行为(methods/方法)。因为
get_name,set_name,get_description 和 set_description 方法在祖先类(Thing)中都已
经存在,所以我们不需要在派生类(Treasure)中重复编码。
Treasure 类有一个附加数据 value(@value),我为该变量编写了 get 和 set 方法。当一个新
的 Treasure 对象被创建的时候,它的 initialize 方法就将自动调用。一个 Treasure 类有三个
变量需要初始化(@name,@description,和@value),所以它的 initialize 方法有三个参数。
前两个参数通过 super 关键字传递给超类(Thing)的 initialize 方法,以便 Thing 类 initialize
方法可以处理它们:
super(aName, aDescription)
当我们在一个方法的内部使用 super 关键字,它将调用父类中与当前方法同名的方法。如果
super 关键字独立使用,并且未指定任何参数,传递给当前方法的所有参数均将传递给父类
中同名方法。如果,和当前案例一样,指定了一个参数列表(这里是 aName 和
aDescription),那么就只有这些参数将传递给父类的方法。
传递参数给超类
在调用超类的时候括号非常重要!如果参数类表为空,并且未使用括号,所有的参数将传递
给超类。但是如果参数列表为空,而使用了括号,那么将没有任何参数传递给超类:
super_args.rb
# This passes a, b, c to the superclass
4. def initialize( a, b, c, d, e, f )
super( a, b, c )
end
# This passes a, b, c to the superclass
def initialize( a, b, c )
super
end
# This passes no arguments to the superclass
def initialize( a, b, c)
super()
end
为了更好的理解 super 的使用,请参考本章末的深入探讨一节
存取器方法
即使这些即将成为冒险游戏的类已经足以正常工作了,但是由于 get 和 set 存取器的存在它
们仍然有太多的冗余。让我们来看看如何解决这个问题。
通常我们通过两个不同的方法 get_description 和 set_description 方法来访问
@description 实例变量的值,如下:
puts(t1.get_description)
t1.set_description(“Some description”)
但是,像下面的这种检索和赋值变量的方式将更加美妙:
puts(t1.description)
t1.description = “Some description”
为了能这么做,我们需要修改 Treasure 类的定义。一个方法是重写@description 的存取器
方法,如下:
5. def description
return @description
end
def description=(aDescription)
@description = aDescription
end
accessors1.rb
我在 accessors1.rb 程序中添加了类似上面的存取器。这里,get 存取器名为 description
而 set 存取器名为 description=(它在相应的 get 存取器方法名后追加了一个等号'=')。现在
可以这样赋值了:
t.description = “a bit faded and worn around the edges”
你还可以获取变量的值:
puts(t.description)
'set'存取器
当你用这种方法编写一个 set 存取器,你必须在方法名的后面添加一个'='字符,而不是在方
法名和参数之间随意放置。
所以这样是正确的:
def name=(aName)
而这样是错误的:
def name = (aName)
属性读取器和修改器
事实上,有一个更加简单更为短小的方式也能达到同样的结果。你所要做的就是使用两个特
殊的方法,attr_reader 和 attr_writer,后接一个标记,如下:
attr_reader :description
attr_writer :description
你可以在你的类定义中添加这些代码,如下:
6. class Thing
attr_reader :description
attr_writer :description
#maybe some more methods here...
end
通过标记调用 attr_reader 可以为一个实例变量(@description)创建一个 get 存取器(在这里
名为 description),该变量与符号(:description)中的名字匹配。
调用 attr_writer 类似地为一个实例变量创建一个 set 存取器。实例变量被视为对象的属性,
这就是为什么 attr_reader 和 attr_writer 方法这么命名的原因了。
标记
在 ruby 中,一个标记是在名字前加一个冒号 ( 例如: description) 。 Symbol 类是在 Ruby
类库中定义的用来表示 Ruby 解释器内的名字。当你传递一个或多个参数给 attr_reader( 这
是一个 Module 类 的方法 ),Ruby 创建一个实例变量和 get 访问器方法。这个访问器方法返回
相对应的变量的值;实例变量和访问器方法都将使用在标记中指定的名字。所
以, attr_reader(:description) 使用该名字创建了一个实例变量 @description 和一个访问器
方法名为 description() 。
accessors2.rb
accessor2.rb 程序中包含了一些属性读取器和修改器的实际运行例子。Thing 类中显式地定
义了@name 属性的 get 访问方法。编写一个这样的完整的方法优点是,你可以在方法中做
一些其他的处理而不只是简单的读取和修改一个属性值。这里的 get 存取器中使用
Str.capitalize 方法来返回@name 初始字符串的大写形式:
def name
return @name.capitalize
end
当我们给@name 属性赋值的时候,我不需要做任何特殊处理,所以我没有对其进行任何特
殊的处理,而只是给了一个属性修改器:
attr_writer :name
@description 属性不需要任何特殊处理,所以我使用 attr_reader 和 attr_writer 来获取和
设置@description 变量的值:
attr_reader :description
11. 然后,根据提示,输入 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}nn”)
end
在该方法中使用了 super 关键字来调用 Thing2 的祖先类或者超类的 initialize 方法。你可以
从它的声明中 Thing2 类的超类是 Thing:
class Thing2 < Thing
在 ruby 中,当一个 super 关键字独立使用时(就是说,没有任何参数),它将传递当前方法
中所有的参数(这里是 Thing2.initialize 方法中所有的参数)给超类中同名函数(这里是
Thing.initialize)。你也可以在 super 关键字后显式的指定一个参数列表。所以,在当前的
案例中,下面的代码将拥有同样的效果:
super(aName, aDescription)
即使可以独立使用 super 关键字,但在我看来,为了代码的清晰度,我更倾向于指定传递给
超类的参数列表。无论如何,如果你只是想传递有限的几个参数给当前方法,那么就必须显
式地指定参数列表。例如 Thing2 的 aMethod 方法只传递 aName 参数给它的超类 Thing1
的 initialize 方法:
super(aNewName)
这就解释了为什么 Thing2.aMethod 调用之后,@description 变量值并没有发生改变。
仔细观察 Thing3 类,你将发现它新增了一个变量@value。在它的 initialize 方法实现中,
它传递了两个参数,aName 和 aDesscription 给它的超类 Thing2。然后 Thing2 类的
initialize 方法依序将这些参数传递给它的超类 Thing 类。
程序运行过程中,输入 3 然后观察其输出。这是这次执行的代码:
t3 = Thing3.new("A Thing 3", "a Thing3 full of Thing and
12. 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
当你运行的时候,注意当 Thing4 对象创建的时候 initialize 方法最先初始化哪个变量。这将
调用 Thing3.initialize 方法,然后,继续调用它的祖先类(Thing2 和 Thing)的 initialize 方
法。然而,Thing4 实现的 aMethod 方法并没有调用其超类的方法,所有这次运行的
aMethod 和祖先类中的任何 aMethod 没有任何关系。
最后,Thing5 类继承自 Thing4,没有新增任何新数据和方法。输入 5 执行如下代码:
t5 = Thing5.new( "A Thing5", "a very simple Thing5", 40 )
t5.aMethod
这次你会看到调用 new 方法,Ruby 会一直顺着继承层级一直回调到它找到第一个 initialize
方法。在 Thing3 中找到了第一个 initialize 方法(它同样是调用 Thing2 和 Thing 中的
initialize 方法)。而 aMethod 的实现发生在 Thing4 中,并且没有调用任何超类中的方法,
回调到此为止。
superclasses.rb
所有 Ruby 类最终均继承自 Object 类
Object 类自身没有任何超类,任何试图找出它的超类的操作都将返回 nil 。
begin
x = x.superclass
puts(x)
end until x == nil
13. 类常量
你肯定会有时候需要访问在泪中声明的常量(以一个大写字母开头)。假设你有这么一个类:
classconsts.rb
class X
A = 10
class Y
end
end
为了能访问常量 A,你需要使用特定的范围限定符::,如下:
X::A
类名是常量,所以这个操作符允许你访问其他类中的类。这让创建如在类 X 内中的类 Y 的内
嵌类成为可能。
Ob = X::Y.new
局部类
在 ruby 中,并不强制在一个地方定义一个类。如果你愿意,你可以在你的程序中分成多个
独立的部分定义一个类。当一个类继承于指定的超类时,每一个后续的局部类定义都可以选
择性地在类定义中使用<操作符重复指定其超类。
这里我创建了两个类,A 和 B,B 继承于 A:
partial_classes
class A
def a
puts( "a" )
end
end
class B < A
def ba1
puts( "ba1" )
14. 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
这给 Array 类添加了一个 gribbt 方法,下面的代码现在可以执行了:
[1,2,3].gribbt