Ruby 101:对象和方法

Ruby 101:对象和方法

 

Written by Allen Lee

 

从静态方法说起

      在上一篇文章末尾,我们提到了受保护的静态方法……受保护的静态方法??Ruby的protected不是用来向相同类型的不同实例开放受限方法的访问的吗(忘记protected的用法了?不要紧,回去上一篇文章复习一下吧。),如果把它用于静态方法,那么我该向参数传入什么?

      在回答这些问题之前,我们先来看看最简单的不带任何参数的静态方法,假设我有一个空的Class1类,如果我试图调用它的method1静态方法,那么我将会被告知没有这个方法:

图 1

在Ruby里,所有类最终都会继承自Object类(这个说法其实不够准确,但就目前而言,你大可放心这样理解),如果我试图调用它的method1静态方法,那么显然,我也将会被告知没有这个方法:

图 2

还记得Ruby允许我们重新打开并修改一个类吗,如果忘记了,不要紧,回去第一篇文章复习一下吧。下面,我们将会通过这种方式向Class类添加method1方法:

图 3

接着,我们再试一次Class1类及其基类的method1方法:

图 4

噢,买瓜!这到底是怎么一回事?

      先别急,解释留到后面,现在让我们把注意力集中到我们的目标上——受保护的静态方法,把上面的发现和上一篇文章的访问控制知识结合起来,就得到创建受保护的静态方法的办法了:

图 5

下面,我们来试一下这个方法:

图 6

显然,我们成功了,接下来,我们使用上面的发现创建一个odd_equals方法,根据length_of_name方法的返回值判断两个类型是否相等:

图 7

最后,我们来试一下这个奇怪的判等方法:

图 8

哇,实在是太不可思议了!我在Class类里创建的实例方法,到了Class1类和Object类就变成静态方法了,难不成……?

      我想你已经猜到了,Class1类和Object类是Class类的实例:

图 9

Class1类和Object类的静态方法则是Class类的实例方法,事实上,Ruby没有静态方法这种说法,这种类似静态方法的东西其实叫做类方法。换句话说,类也是对象……

 

类也是对象

      类也是对象?如果类也是对象,那么Class类是谁的实例?答案是Class类自己:

图 10

换句话说,它是一个Class对象。还记得Object类吗?嗯,Object是一个类,而类又是对象,于是, Class是一个类,Object是一个类,Object是一个对象,Class是一个对象……从类的角度来看,Class类应该先于Object类而存在;而从对象的角度来看,Object对象应该先于Class对象而存在,哈哈,我们陷入先有鸡还是先有蛋的悖论了……嘿嘿,你可别真的陷进去了哟,如果你感到混乱,那就忽略它吧,就目前而言,你只需要记住类也是对象就行了。

      如果类也是对象,那么它也应该可以拥有实例变量,但这种实例变量有什么用呢,它和我们之前看到的实例变量有什么不同呢,它和类变量又有什么联系呢?我们知道,实例变量是和实例绑定的,创建实例变量最直接的做法是在实例方法里执行赋值操作,假设我们有一个Class1类,如果我们想为它创建实例变量,那么我们就要回到Class1类的类里,嗯,读起来有点新鲜和拗口,但操作起来却非常简单:

图 11

@value1变量是Class类的实例变量,目前只能通过set_value1方法设值,而set_value1方法又是Class类的实例方法,只能通过Class类的实例访问,于是,若要调用set_value1方法,就得先有Class类的实例,那么,谁是Class类的实例?就上图来而言,Class1类和Class类自己都是Class类的实例,为了避免思维过于纠结,我们还是选择Class1类吧:

图 12

如果我想获取或者输出@value1变量的值呢?最直接的做法当然是在Class类里创建一个方法,然而,仔细观察set_value1方法的调用,你会发现它和我们在前两篇文章里看到的类方法没啥两样,这是否意味着我们在Class1类里创建的类方法也能访问@value1变量呢?试一下就知道了:

图 13

噢耶,成了!现在,请思考一下:我们在Class1类里创建的实例方法能否访问@value1变量?为什么?对于第一个问题,我们试一下就知道了:

图 14

从上图可以看到,@value1变量对于Class1类的print_value1实例方法是不可见的,这是否意味着,我们在Class类里创建的@value1变量和我在Class1类里创建的@value1变量是互不相干的呢?嗯,试一下就知道了:

图 15

答案已经很明显了,但是,为什么呢?它们的名字是一样的,使用的时候又没有附加任何标识,Ruby究竟是如何区分它们的呢?秘密就在self上!还记得Ruby如何限制私有方法的调用吗(如果忘记了,不要紧,回去上一篇文章复习一下吧),在Ruby里,任意一个时刻都有一个默认对象,用self来表示,上面两个@value1变量之所以相互独立,是因为在Ruby碰到它们的时候,self分别指向两个不同的对象,在类方法里,self指向类,而在实例方法里,self指向类的实例:

图 16

换句话说,上面两个@value1变量隶属于两个不同的对象,自然不会混到一起,事实上,我们通常会把这种和类绑定的实例变量叫做类实例变量(class instance variable),以便和一般的实例变量区分开来。

      现在,请思考一下:在类的定义以内、任何方法的定义以外的地方,比如说,上图的第一行和第二行之间,self指向什么?嗯?这种夹缝之地也有默认对象?当然有:

图 17

从上图可以看到,此处的self和类方法里的self一样,都是指向类的,这是否意味着,我们也可以在里创建类实例变量?我们不妨试试看:

图 18

从上图可以看到,这里也可以创建类实例变量,那么,这种做法和前面的有什么不同呢?你可能会说,前面创建的@value1变量在Class1类和Class2类里都可用,而这里创建的@value2变量只能在Class2类里可用,到底是不是这样呢,我们试一下就知道了,为了避免干扰,我们使用一个干净的Class3类来做试验:

图 19

从上图可以看到,在我们调用在Class类里创建的set_value1实例方法之前,@value1变量根本就不存在!为什么?因为Ruby的实例变量是按需创建的,在你对它们进行赋值操作之前,它们是不存在的(如果你忘记这部分内容了,不要紧,回去上一篇文章复习一下吧),换句话说,Class3类并没有从第一种做法那里占到什么便宜,事实上,同一类型的不同实例在某个时刻也可能有着完全不同的实例变量。

      说到这里,你可能会问,类变量和类实例变量都属于类,那么它们之间有什么区别?第一,类实例变量只能被类方法直接访问,而类变量则可以被类方法和实例方法直接访问;第二,类实例变量的值和单个类挂钩,而类变量的值则和某个继承体系挂钩。来,猜猜下面代码的输出是什么:

代码 1

答案是3、4、4。什么?最后一个竟然是4?没错,因为类变量的值是和整个继承体系而不是单个类挂钩的,当我们创建Rectangle类的实例时,我们改变了@@sides变量的值,这将影响整个继承体系,我们期望变量的值在类的各个实例中保持一致,但在不同的类中保持独立,此时就需要类实例变量了:

代码 2

这样输出就没问题了。值得注意的是,类实例变量不能被实例方法直接访问,为了能在实例方法里访问它,我们得先为它创建一个类方法,用于返回它的值,然后通过self.class获得当前实例的类,最后通过这个类访问刚才创建的类方法,从而获取类实例变量的值,道路似乎非常曲折,不过,如果你把类和它的实例看作两个独立的对象,那么理解起来应该畅顺很多的。

      朋友们,请系好安全带,因为接下来的旅程将会更加惊险!

 

对象的方法

      既然类是Class类的实例,这是否意味着,我们可以通过Class类的new方法创建一个类,并通过这个类的new方法创建它的实例?我们试试看吧:

图 20

正如你看到的,这条路行得通。如果我想为Triangle类提供一个sides类方法,用于返回三角形的边数,我该怎么做呢?既然Triangle类是Class类的实例,你可能会说,在Class类里创建不就行啦?好,我们试试看:

图 21

看起来似乎没问题,但是,如果我再创建一个Rectangle类,然后调用它的sides方法,那么问题就来了:

图 22

很显然,这不是我们想要的,怎么办?有人可能会建议,既然Triangle类和Rectangle类都是Class类的实例,不妨考虑使用@sides变量来保存边数,然后在sides方法里返回它,@sides变量可以通过Class类的initialize方法初始化。好,我们试试看,但为了避免干扰,我们打开一个新的irb:

图 23

看起来似乎没问题,如果我再创建一个Person类呢?

图 24

噢,出错了!怎么回事?看看错误信息,原来是参数的个数不匹配,看到这里,你可以会说,为initialize方法提供一个重载吧。好,我们试试看:

图 25

看起来似乎没问题了,如果我再创建一个Circle类呢?

图 26

又出错了!为什么会这样?原来,Ruby不支持方法重载,我们在图25里创建的无参initialize方法将会覆盖前面那个带参的!显然,我们不能通过initialize方法来初始化@sides变量了,一个可能的做法是为@sides变量提供一个写访问器:

图 27

这下应该没问题了吧?且慢!Person类根本不需要sides方法!

      毫无疑问,我们陷进共性和个性的问题了,只有部分类需要sides方法,而这些类的sides方法又可能返回不同的值。嗯?这有什么好奇怪的,我们平时就是这样理解的呀,反而特意在这提出来才奇怪呢!是的,如果你用我们过往的经验来理解,这里似乎没有什么问题,但如果你换个角度,就会看到我们现在面临的问题了。想想看,类是什么?Class类的实例。相同的类型,部分实例拥有某些方法,部分没有,这意味着什么?噢!反应过来了吗?从另一个角度来看,对象的行为取决于先天和后天两个因素,先天是指我们在类(或模块)里创建的实例方法,它们适用于类的所有实例,那么后天呢?后天是指我们为单个对象创建的方法,它们只适用于单个对象,Ruby把这种方法叫做单例方法(singleton method)。

      那么,如何创建单例方法呢?非常简单,就像你创建普通的实例方法那样,不同的是,你要在方法名字前面加上对象名字,两个名字之间用.分隔。为了避免干扰,我们再开一个新的irb:

图 28

不会吧?又来一种新的写法??嘿嘿,先别急嘛,有没有觉得这个写法似曾相识?细心的你可能已经发现,它和我们在第一篇文章里看到的第一种创建类方法的写法是一样的,除了那个是写在类的定义里,而这个是写在外面的。下面,我们把那篇文章介绍的头两种写法和这里介绍的放在一起看看:

代码 3

当你看到上面这个代码时,有没有这样一种感觉,它们其实是一样的,如果有,那么恭喜你,你的感觉是对的。我们知道,在Ruby里,self指向默认对象,而在类的定义里,默认对象就是类自己,所以上面的def self.boiling_pointdef Mercury.boiling_point是一样的。那么,剩下的两种写法呢?我们还是把它们和这里介绍的写法放在一起看看吧:

代码 4

我们知道,第三种写法的self其实就是Mercury类,换句话说,它和第四种写法的区别仅仅在于一个在类的定义里,一个在外面,但是,这只是表面上的区别,实际上它们是一样的,第四种写法也可以写在类的定义里,你可以自己试试看。那么,第四种写法和第五种又有什么区别呢?这要从单例方法的容身之所说起,我们知道,实例方法存在于类或模块里,能够调用它们的对象可以在这里找到它们,那么单例方法呢,如果它们只能被单个对象调用,对象又该到哪里才能找到它们?现在,细心观察一下第四种写法,你觉得它想什么?是不是很像类的定义?对了,它其实就是一个类,专门用来存放单例方法,Ruby把这种类叫做单例类(singleton class)。每个对象都有两个类,一个是用来创建实例的类,另一个则是单例类,单例类是自动创建的匿名类,但你可以通过第四种写法重新打开它,并往里面添加单例方法。换句话说,第四种写法和第五种都是用来创建单例方法的,前者直接在对象上创建,后者在对象的单例类里创建,它们之间有一个小区别,但就目前而言,你可以把它们看作一样的。

      数一数,我们现在有多少种创建类方法的写法?5种(如果把第四种写法放在类的定义里也算上的话就是6种了),而这些写法最终都可以统一为为类创建单例方法,从另一个角度来看,类方法其实是单例方法的一个具体应用。既然(作为对象的)类可以创建单例方法,那么普通对象也肯定可以:

图 29

现在,请思考一下:类的单例方法和普通对象的是否有什么不同?想想看,类方法能被谁调用?包含该方法的类及其子类。普通对象没有子类的概念,这意味着,类的单例方法是和一个继承体系挂钩的,而普通对象的单例方法只和单个对象挂钩。

      前面我们提到,单例类是匿名类,那么我们是否可以自己创建匿名类?可以的,在Ruby里,类名遵循常量的命名规则,一般建议使用Pascal命名方式而不是全部大写,如果你通过Class类的new方法创建一个类,并把它赋给一个常量,那么它就是命名类,如果你把它赋给一个变量,那么它就是一个匿名类:

图 30

你也可以在创建匿名类的同时为它创建实例方法:

图 31

这里,我们向Class类的new方法传递一个代码块(code block),如果你对它没有了解,不用担心,这个东西将在下次详述,现在你只需知道它可以用来传递一份代码就行了。值得提醒的是,calc_result是一个匿名类而不是匿名对象,这意味着如果你要使用它的实例方法,你得先创建一个实例:

图 32

当然,这不是问题,因为你可以把匿名类的创建和实例化放在一起,毕竟,使用匿名类的潜台词是你不打算重用它的定义:

图 33

那么,如果两个匿名类的结构和内容都一样,它们的实例也相等吗?我们试一下就知道了:

图 34

显然不相等,那么,是否有办法让它们相等?试想一下,person1 == person2是什么?表面上,我们似乎在使用==运算符,但实质上,我们其实在调用==方法,换句话说,person1 == person2将会被解析成person1.==(person2) ,于是,若要person1 == person2的结果为true,只需为person1对象重写==方法就行了:

图 35

值得提醒的是,我们不必为==右边的对象重写==方法,除非你需要想在==上实现对称性。此外,如果你还想对它们使用其它比较运算符,如<>等,你可以把Comparable模块包含进来,然后创建<=>方法就可以了:

图 36

然而,这并不意味着book2 > book1也能执行,因为这些运算符最终都被解析为方法调用,如果你想实现这种等价效果,book2对象也要包含Comparable模块和创建<=>方法。

 

向对象发送消息

      我们知道,面向对象强调通过对象之间的协作来完成任务,那么,对象之间是如何通信的呢?回想一下,我们平时是如何让对象执行操作的?调用对象的方法。就上面的图29而言,我们想让唐老鸭游泳,就调用它的swim方法,从另一个角度来说,我们向它发送swim这个消息,请求它执行对应的操作,在Ruby里,我们可以通过send方法显式向对象发送消息:

图 37

如果你是第一次看到这种做法,可能会觉得它没有直接调用方法来得直观,然而,它却拥有一些后者没有的特殊好处,比如说,如果你想让对象执行的操作是由用户或者配置文件来指定的,那么,你要么通过一个庞大的条件语句来判断要调用的是哪个方法,要么通过一个字典来维护用户或者配置文件指定的操作名字和对应方法之间的映射,如果换用send方法,那么你只需把表示操作名字的字符串传给它就行了。

      现在,请思考一下,如果我们可以向send方法传递任意字符串,并且可以重写它目前的实现,这意味着什么?这意味着我们可以为对象实现一个消息代理!如果某个功能有新旧两个实现,那么我们可以通过这个消息代理把请求从旧版本重定向到新版本,或者根据一定条件来决定应该使用哪个实现。假设我们为唐老鸭创建了一个swim_at_speed方法:

图 38

我们希望重用swim这个消息,并根据用户是否提供速度来决定应该使用哪个实现,那么我们可以这样重写send方法:

图 39

因为send方法的第一个参数可以是符号类型(:swim)或者字符串类型("swim"),而符号类型的比较效率比字符串类型的高,所以我们统一通过to_sym方法把方法的名字转成符号类型再行比较。而第二个参数则是可变参数的写法,它实质上是一个数组。细心的你可能已经注意到代码中的super关键字,在Ruby里,当你调用一个方法时,Ruby会遵循一定的规则查找这个方法,查找的工作会沿着满足这些规则的某条路径展开,在这条路径上可能会遇到多个可用的实现,但只有最先遇到的幸运儿才会被调用,而super关键字则指向当前方法在这条路径上的下一个实现。下面,我们来试试这个消息代理:

图 40

既然我们可以拦截消息,这意味着我们还可以在调用目标方法之前和/或之后执行某些代码,嗯,有点AOP的味道。

      说到这里,你可能会问,如果某个消息没有与之对应的方法呢?这个好办,你可以为对象提供一个类似"404页面"的实现,把没有方法与之对应的消息重定向到这里,但是,这种做法仅对通过send方法发送消息才有效,如果是直接调用方法的,就要通过重写method_missing方法来处理了:

图 41

此外,method_missing方法有一些很有趣的应用,比如说,Builder,它通过重写method_missing方法把这样的方法调用:

代码 5

转换成这样的XML输出:

图 42

既然我们可以把方法调用重定向到XML输出,我们也可以把方法调用重定向到其它对象上,比如下面的BookStore类:

代码 6

除了读取/保存数据这部分逻辑之外,其它诸如添加新项等功能就直接通过send方法转给内部的数组,想想看,如果数组的每个方法都要在BookStore类里创建一个对应的方法,这不写死人吗?当然,你也可以设定规则,只把部分功能转给内部的数组。细心的你可能已经发现上面代码出现了两个新的面孔,一个是&block,这是传递代码块的语法,另一个是bs <<右边的那片代码,这是字典的语法,如果你对它们感到陌生,不要紧,它们都会成为将来某篇文章的主角,但现在不能喧宾夺主。

 

新的旅程

      今晚,听着陈奕迅的《床头灯》,突然,一股莫名的激动涌上心头,我仿佛从这首歌里听到了自己,于是,我特意打开歌词,一个字一个字地听……"我庆幸我走在一条不完美的道路,认清我们多渺小多么脆弱"……"我庆幸我身在这场没脚本的演出,领悟这个姓名该起的作用"……"作过的梦还倒背如流,只是有了不同的感受"……

      有一次,怪怪和我聊到写博有何好处,嗯,我喜欢不断地学习,学习我喜欢的东西,如果你把人比作杯子,把学习比作往杯子里倒水,那么把学到的东西困起来就相当于把水留在杯子里,久而久之,杯子就无法装进新的水了,对我来说,写博就像把杯子倒空,这样我就可以轻装上阵,学习更多新的东西了。事实上,如果你拿我曾经写过的东西来考我,很多时候都会把我考倒,因为写下之后我就会把细节忘掉,所以我总是把东西写得尽量详细。换句话说,学习和写博共同组成了一条让知识经我而过的通道,说到这里,我想起曾经在一篇文章里读到现代舞大师玛莎·格雷厄姆(Marthe Graham)的一段精彩描述:

有股活力、生命力、能量由你而实现,从古至今只有一个你,这份表达独一无二。如果你卡住了,它便失去了,再也无法以其他方式存在。世界会失掉它。它有多好或与他人比起来如何,与你无关。保持通道开放才是你的事。

或许,正如陈奕迅所唱的,我也正在试图"领悟这个名字该起的作用"……

      Ruby宣称一切皆对象,到目前为止,我们已经看过很多很多对象了,甚至连类和模块本身也是对象,但有一个东西我们还没有从对象的角度研究过的,那就是方法,下一次,我们将会围绕这个话题,探索更多有趣的秘密……

posted @ 2009-11-03 20:01 Allen Lee 阅读(...) 评论(...) 编辑 收藏