Ruby 101:类和对象

Ruby 101:类和对象

 

Written by Allen Lee

 

今天开始Ruby ~

       虽然仅仅阅读文章也能了解Ruby的语法,但这样就会少很多乐趣,如果你有兴趣学习Ruby,我建议你还是动手试试,与纯粹阅读相比,亲身体验将会有另一番不同的感受。

       工欲善其事,必先利其器,想要体验Ruby,就得花点时间配置它的运行环境了。目前可以选择的有RubyIronRubyJRuby,你可以根据自己的喜好/需要选择其中一个,其中,IronRuby需要Microsoft .NET或者Mono的支持,而JRuby则需要JVM的支持。

       Ruby运行环境的安装和配置非常简单,到官网下载最新版的压缩包,把它解压到C:\(或者你喜欢的其他地方),然后把C:\Ruby\bin(或者你所选择的路径)添加到Path环境变量,这样就好了!(这是Windows平台的安装方法,如果是Ubuntu,可以通过Package Manager直接安装。)对于IronRuby,如果你已经安装了Visual Studio 2005/2008,那么你只需从codeplex.com/ironruby上下载最新版的压缩包,把它解压到你喜欢的地方,然后把bin的路径添加到Path就可以了。对于JRuby,除了上述方法之外,你还可以下载并安装NetBeans的Ruby专用版,它已经自带JRuby 1.2.0,当然,如果你想要最新的1.3.1,那就要自己动手了。

       接下来,打开运行对话框,输入irb,然后按下回车,Ruby的命令行运行环境将会打开,在里面敲下puts "Start Ruby today ~"并按下回车,这将会向控制台输出"Start Ruby today ~":

图 1

puts是一个方法,它负责把数据输出到控制台,在Ruby里,调用方法时通常都是可以省略括号的,当然,如果你已经习惯带括号的调用风格,你也可以把它加上。恭喜你成功运行了你的第一份Ruby代码!

 

模块、类和对象

       在Ruby里,创建类型是非常简单的,假如我要创建一个Book类,我可以这样做:

代码 1

       接下来,我们创建一个Book的实例对象:

代码 2

b是一个变量,在Ruby里,使用变量时无需声明它的类型,事实上,变量不和类型关联,你完全可以在后面把它用于其他类型的对象(第二个b下面的波浪线是NetBeans提示变量未被使用):

代码 3

而这在静态语言里面是不允许的。回到代码2,我们用new方法创建一个Book的实例对象,但这个new方法从哪里来的呢?它是Ruby的解析器为我们自动创建的默认构造函数吗?不是的,不过,它确实是构造函数。糊涂啦?不要紧,我们会在后面详细探讨,现在你只要记住可以用它来创建对象就行了。

       一般情况下,我们会把类组织到不同的命名空间里,在Ruby里,我们用模块来实现命名空间的功能,比如说,我想把Book类放在Ruby101模块里,我可以这样做:

代码 4

当我们把类放进模块,我们就需要通过模块的名字来引用类了:

代码 5

 

实例成员

       现在的Book类空空如也,我想没有人会想要这样一个没用的类,如果想用它来存储一些有用的信息,我们就需要为它创建一些实例字段。

       下面,我将会为Book类创建一个initialize方法,用来初始化Book的实例字段:

代码 6

在Ruby里,方法的命名方式也是有规定的,它可以包含字母、数字和下划线(少数特殊字符也允许出现在方法名字里,比如!?=,但它们的使用通常有一些约定俗成的意义),并以小写字母或者下划线开头。代码6里的构造函数接受4个参数,这些参数都没有声明类型,这和变量一样,构造函数里分别初始化4个实例字段。字段?慢着!我们还没有定义……

       在Ruby里,实例字段都是以@开头的,并且在使用之前无需事先声明,但这些实例字段都是私有的,要从外面访问这些字段,我们需要为它们创建访问器:

代码 7

代码7示范了3种不同风格的访问器,你可以根据个人喜好选择其中一种,一般情况下,我们会采用第三种风格,事实上,第三种风格最终会被"还原"成第一种风格,在某种程度上,你可以把它理解成C# 3.0的自动属性,除非你要向访问器添加(验证)逻辑,否则你没有必要直接使用第一种风格。在Ruby里,方法的最后一个表达式的值将会自动作为方法的返回值,当然,你也可以显式使用return来返回。attr_accessor实际上是一个方法,它的职责就是根据我传给它的名字生成第一种风格的代码,:price是我传给它的参数,:XXX用来表示Ruby的Symbol类型的对象,表面上它和字符串很像,但内里却有着很大不同,就目前而言,你只需把它们之间的区别理解为相同内容的Symbol对象在内存里只存在一份,而相同内容的String在内存里则会存在多份(如果你有兴趣深入了解Symbol,可以阅读Eric Kidd的《13 Ways of Looking at a Ruby Symbol》),和attr_accessor对应的还有attr_readerattr_writer,顾名思义,它们分别用来创建只读和只写访问器。

       下面,我们来看看这些访问器分别是如何使用的:

代码 8

正如你所看到的"title = XXX"实际上会被解析成对title=方法的调用,当然,你也可以使它变得更明显些:b.title=("The Ruby Programming Language")。在调用set_authors方法时,我向它传递一个数组,在Ruby里,数组表示为[XXX, YYY, ZZZ]

       现在,我们统一使用第三种风格为所有实例字段创建访问器:

代码 9

下面,我们创建一个Book的实例对象,并输出它的信息:

代码 10

       如果你和我一样都是使用NetBeans,那么你只需要在编辑器任何地方右击鼠标,然后选择Run File就可以运行Ruby代码了:

图 2

运行结果将会显示在Output窗口里:

图 3

在NetBeans里,你可以通过File ->Project Properties轻松切换Ruby的运行环境:

图 4

       当然,你也可以使用记事本或者其他你喜欢的文本编辑器来编写Ruby代码,然后把代码保存为XXX.rb文件,并通过命令行用Ruby解析器执行代码,下面分别使用Ruby和IronRuby来执行上面的代码:

图 5

如果你嫌代码9还不够简化,那么你可以试试Ruby的Struct

代码 11

代码11和代码9是等效的,就像attr_accessor方法那样,Struct最终会帮你生成代码9里的Book类,而用法上和之前是一样的,如果你想创建的是仅用于承载数据的类,那么Struct就是为你量身定做的了。如果将来你想扩展Book类,比如说,为它添加方法怎么办?没问题,既然Struct为你创建的是一个类,那么你可以通过继承扩展这个类,此外,你还可以"直接"扩展这个类。什么意思呢?Ruby允许你修改任何类的定义,即使是内置的类,所以,你可以在代码11后面"重新打开"Book类,并往里面添加方法:

代码 12

由于tags是一个数组(确切地说,应该是tags方法的返回值是一个数组,虽然这只是我们的期望,并且任何人都可以打破它),我们可以通过<<向数组添加元素,值得提醒的是,Ruby的数组和我们通常使用的数组不同,它的长度是可变的。

       此外,Ruby还提供了OpenStruct,和Struct不同的是,OpenStruct创建出来的是对象而不是类,说到这里,你可以感到奇怪,如果是对象的话,自定义的属性要如何设置呢?这正是OpenStruct的独特之处,自定义的属性会在第一次使用时自动创建!下面,我在irb里示范用OpenStruct创建Book对象:

图 6

首先,我通过require引用OpenStruct的库,接着,我通过new方法创建了一个OpenStruct对象,然后,我通过反射查看这个对象提供的方法,随后,每设置一个属性的值,我就通过反射来查看这个对象的方法,正如你所看到的,自定义的属性确是在第一次使用时自动创建。另外,你有没有发觉,这次的irb和前面看到的不一样?这是因为我在启动它的时候使用了--simple-prompt命令行选项,这可以简化irb提示符的显示,你可以对比图1感受一下它们之间的区别。

 

类成员

       现在,我需要一个书架帮我管理我的书,这个书架提供存放和查找功能,我们将会通过这个书架了解Ruby的类成员的写法。

       下面,我们创建一个BookShelf类,并且在它里面创建一个静态的哈希表,用来存放Book对象:

代码 13

在Ruby里,静态字段以@@开头,{}则用来创建一个空的哈希表。接下来,我们要创建两个静态方法:placefind,分别用于存放和查找Book对象:

代码 14

在Ruby里,创建静态方法是非常简单的,代码12示范了两种风格,第一种是把类名置于方法名字之前,中间用.分开;第二种是把self置于方法名字之前,中间用.分开,这两种风格是等效的,区别只在于个人喜好。哈希表可以通过[]来访问里面的数据,当你看到代码12时,你可能会问,为什么place方法和find方法都没有事先检查key是否存在,你可以这样做,但没有必要,因为在调用place方法时,如果key已经存在,则用新的value代替现有的,如果key并不存在,则往里面添加这对key/value,而在调用find方法时,如果key已经存在,则返回对应的value,如果key并不存在,则返回nil,这些是哈希表的[]预设的行为。

       除了上述两种风格,Ruby还支持另外两种看起来有点怪异的风格,第一种是创建一个实例方法,然后把它包在class << selfend之间:

代码 15

这种做法咋看比较怪异,但有时候确是必须的,比如说,我现在想用attr_accessor来为path创建静态字段和读/写访问器,显然,之前介绍的两种做法都无能为力,因为最终代码是由attr_accessor方法代为生成的,这时就轮到class << self出场了,我只需把attr_accessor :path放在class << selfend之间就行了。

       第四种风格可以看作第三种的变形,它们之间的区别在于第三种风格的代码是放在类里面的,而第四种风格的代码则放在类外面,于是,class << self需要改成class << BookShelf,以便告知这份代码的所属:

代码 16

       现在,我们来看看BookShelf的使用情况(变量b的初始化请参见代码10):

代码 17

运行结果如下:

图 7

Ruby支持字符串插值(String Interpolation),当我们用#{}包围某个表达式时,这个表达式的运算结果将被插入表达式所在的位置,从运行结果可以清楚地看到这点。另外,我在puts后面添加了unless XXX,这将导致puts语句在unless后面的条件满足时不执行,当然,如果你不嫌冗长,你也可以把它改成:

代码 18

效果都是一样的。

       现在,回到前面的问题,new方法是从哪里来的呢,还有,我们定义的initialize方法好像从来没有调用过,又是谁给那些私有字段初始化的呢?下面,我们将会使用irb来探个究竟:

图 8

首先,我创建了一个Book类,里面只有initialize方法,当我用new方法创建一个Book对象时,我们从命令行的输出结果可以看到initialize方法已被调用,接着,我对Book类进行扩展,添加一个new方法,从前面的调用来看,它是一个静态方法,因此,你可以选择上述四种风格的任意一种来实现,然后,我再用new方法创建一个Book对象,这次,我们从命令行的输出结果可以看到new方法和initialize方法都被调用,而且new方法的调用在initialize方法之前。细心的你可能已经发现,new方法的实现里包含了一个super关键字,这是什么意思呢?在回答这个问题之前,我们做另一个实验,就是把这个关键字去掉,看看会有什么不同:

图 9

当我们把super关键字去掉之后,从命令行的输出结果不难发现,initialize方法"失效"了,并且new方法不再创建对象!综上所述,new方法是initialize方法得以调用的关键,事实上,new方法是Book类从Class类那里继承过来的一个方法,它的职责就是创建(未初始化的)对象,并调用initialize方法来初始化这个对象(initialize方法是私有的,你无法直接调用它),我们刚才为Book类重写了这个方法,为了使它依然生效,我们需要调用"原本的"实现,因此需要加上super关键字,就目前而言,你只要记住super关键字的作用是调用当前方法的继承版本就行了。

 

新的旅程

       上一节末尾,我们提到了继承,这是面向对象编程的核心概念之一,那么,Ruby又是如何体现继承以及面向对象编程的其他特征呢,我们将会在以后的文章里逐一探讨。

 

P.S. 本文的代码,若无特别说明,均可在Ruby、IronRuby和JRuby上运行,笔者所使用的版本分别是1.9.1、0.9.0和1.3.1。

 

posted @ 2009-09-25 08:26  Allen Lee  阅读(5552)  评论(11编辑  收藏  举报