Diese Präsentation wurde erfolgreich gemeldet.
Die SlideShare-Präsentation wird heruntergeladen. ×
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Wird geladen in …3
×

Hier ansehen

1 von 14 Anzeige

Weitere Verwandte Inhalte

Anzeige

第二章

  1. 1. 第二章 类继承,属性和类变量 上一章的末尾,我们创建了两个新的类:Thing 和 Treasure。尽管事实上着两个类有很多的 共同之处(特别是两个类都有一个 name 属性),但是它们之间没有任何关系。 现在,着两个类都还是很简单的,它们之间存在一些重复关系也不大。然而当我们开始进行 真正复杂的编程时,你的类经常都包含很多的变量和方法,而你又不想一次又一次地重复进 行同样的编码。 很有必要创建类继承,在继承中,某个类是其他类(祖先)的特殊类型,这就意味着它将自动 继承它的祖先所有特征。在我们简单的历险游戏中,举例说明,Treasure 是 Thing 的一种 特殊类型,所以 Treasure 可以继承 Thing 类的特征。 类继承--祖先和派生类:在本书中,我将经常谈论“派生类”继承“祖先类”的特征。这 些术语刻意得让人联想到这些相关类的关系如同一个家庭。在 Ruby 中的每一个类都只有一 个父类。虽然,它可能继承于一个很长的很明显的有家庭树,有很多的父类,祖父类,曾祖 父类等等 Thing 类们的基本行为都将在 Thing 类中编码。Treasure 类将自动从 Thing 类继承它所有 的特征,所以我们不需要在 Treasure 类中重复编码了;我们可以添加一些附加要素, Treasure 类特殊的要素。 基本原则就是,在创建一个类继承的时候,拥有最多基本行为的类在继承层次中要比拥有更 多特定行为的类层次要高。所以,一个 Thing 类中只有一个 name 和 description,将作为 Treasure 类的祖先类,Treasure 类不仅有 name 和 description 属性,还有其他的属性, 如 value;Thing 类同样也可以是别的类的祖先类,例如 Room 类有 name,description 以及 exits 等等。
  2. 2. 这个图表描述了 Thing 类包含了一个 name 和 description( 在 Ruby 程序中,这些应该都是内部变量,如 @name 和 @description 和一些访问它们的方法 ) 。 Treasure 和 Room 类均 继承于 Thing 类,它们自动继承了 name 和 description 属性。 Treasure 类新增了 value ,现在有 name,description 和 value 属 性, Room 新增了 exits ,现在有 name,description 和 exits 属 性。
  3. 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. 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. 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. 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
  7. 7. attr_writer :description 属性或性质? 不要因为这些术语而迷惑。在 Ruby 中,属性是和其他编程语言中的性质是相同的 当你想可以读取和修改一个变量的时候,attr_accessor 方法提供了一种更为简短的方法, 用来替代 attr_reader 和 attr_writer 方法。在 Treasure 类中我已经使用了这个方法来防卫 变量属性: attr_accessor :value 这和下面的代码效果是一样的: attr_reader :value attr_writer :value 之前我说过通过符号来调用 attr_reader 实际上创建了一个与符号同名的变量。 attr_accessor 方法同样是这么做的。 在 thing 类的代码中,这个行为还不是特别明显,因为这个类有一个 initialize 方法显式地 创建了这些变量。然而在 Treasure 类的 initialize 方法中,没有任何针对@value 的引用。 @value 仅在存取器的定义处出现过: attr_accessor :value 在源文件中,我在下面的代码中针对每个 Treasure 对象,在其创建之后,进行了单独的赋 值操作: t1.value = 800 即使它从来都没有正式地声明,@value 变量确实存在,并且我们可以通过 get 存取器来检 索其数值: t1.value 为了确确实实地验证属性存取器真的创建了@value,你可以经常使用 inspect 方法来查看 类的内部。我已经在这个程序的最后两行中这么做了: puts “This is treasure1:#{t1.inspect}” puts “This is treasure2:#{t2.inspct}” accessors3.rb 属性存取器可以一次初始化多个属性,如果你传递给它一个符号列表,所有参数间使用逗号
  8. 8. 进行隔开,如下: attr_reader :name,:description attr_writer(:name,:description) attr_accessor(:value,:id,:owner) 与往常一样,在 Ruby 中,参数外的括号可以省略,但是我的观点(为了代码的清晰度/可读 性)是,最好加上括号。 2adventure.rb 现在让我们来看看如何在我的冒险游戏中使用这些属性读取器和修改器。加载 2adventure.rb 程序。你将看到我在 Thing 类中创建了两个可读的属性:name 和 description。我也将 description 设置为可写的了,然而,我并不胆酸改变任何 Thing 对 象的名称,所以 name 属性是不可写的: attr_reader(:name, :description) attr_writer(:description) 我创建了一个 to_s 方法,它返回一个描述 Treasure 对象的字符串。重申一下,所有的 Ruby 类都有一个 to_s 方法。Thing.to_s 重载(替代)了默认的那个 to_s 方法。当针对特殊的 类型需要实现一些新行为的时候,你可以重载已有的方法。 调用超类的方法 我决定我的游戏将有两个类继承于 Thing。Treasure 类添加一个可读写的 value 属性。注意 它的 initialize 方法调用它超类的方法,在初始化新的属性@value 变量之前初始化 name 和 description 属性: super(aName, aDescription) @value = aValue 如果我在这里没有调用超类的方法,那么 name 和 description 属性将永远不会得到初始化。 这厮因为 Treasure.initialize 重载了 Thing.initialize 方法;所以当一个 Treasure 对象创建 之后,Thing.initialize 中的代码将不会自动执行。 另一方面,Room 类同样继承自 Thing 类,没有 initialize 方法,所以当一个新的 Room 对 象创建时,Ruby 在类继承层次中向上搜索一个 initialize 方法。第一个 initialize 方法在 Thing 类中找到了,所以 Room 对象的 name 和 description 属性在那里进行了初始化。
  9. 9. 类变量 在这个程序中还有一些其他有趣的事情。在 Thing 类的顶部,你将看到: @@num_things = 0 在变量名前的两个@字符,定义其为类变量。到目前为止,我们在类中使用的变量都是实例 变量,在变量前加一个@,如@name。鉴于类的每个新对象(或实例)给它自身的实例变量赋 一个特定值,所有继承于某个指定类的对象共享相同的类变量。我给@@num_things 变量 赋值为 0 确保它在外部有一个有意义的值。 这里,@@num_things 类变量被用于保存游戏中运行的 Thing 对象总数。这个很简单,在 新对象创建的时候,在它的 initialize 方法中增加类变量的值(自增 1:+=1): @@num_things += 1 如果你继续看我的代码,你将发现我创建了一个 Map 类来包含一个 rooms 的数组。包含了 一个 to_s 方法,它可以打印出数组中每一个 room 对象的信息。不要担心 Map 类的实现, 我们将在下一章中继续数组和他们的方法的探讨。 滚动代码到文件的底部,然后运行程序,来看看我是如何创建并初始化了所有的对象,并使 用类变量@@num_things 保存创建了的 Thing 对象的总数。 类变量和实例变量 这个图表描述了一个 Treasure 类 ( 矩形 ) ,它包含了一个类变量 @@num_things 和一个实例变量 @name 。三个椭圆形表示 Thing 对 象, Thing 类的实例。当这些对象中的某一个给它的实例变量 @name 赋值 后,这个值只会影响到这个对象自身的 @name 变量。所以,在这里每一个对象 拥有一个不同的 @name 值。但是当一个对象赋值给类变量 @@num_things 时,存活于 Thing 类内部的变量为该类所有实例所共享。这里 @@num_things 等于 3 ,并且在所有的 Thing 对象中都是 3 。
  10. 10. 深入探讨 超类 super.rb 为了了解 super 关键字如何工作,我们来看一下我的示例程序,super.rb。这个程序中包含 了 5 个相关的类:Thing 类是所有其他类的祖先类,Thing2 类继承自 Thing 类,Thing3 继 承于 Thing2,然后是 Thing4 和 Thing5。 让我们更深入地来看看在这个继承层次中的三个类:Thing 类有两个实例变量,@name 和 @description;Thing2 还定义了@fulldescription(一个包含@name 和@description 的 字符串);Thing3 添加了另外一个变量@value。 这三个类各自包含一个 initialize 方法,当新对象创建时设置变量的值;也各自有一个方法 名为 aMethod,该方法改变一个或多个变量的值。这些派生类,Thing2 和 Thing3,均在 他们的方法中使用了 super 关键字。 在命令行中运行 super.rb 。测试不同的代码片段,输入数字 1 到 5 ,输 入 'q' 退出 在该代码的底部,我写了一个'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 变量的字符串值。
  11. 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. 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. 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. 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

×