Ruby对象模型
在了解了对象和消息之后,让我们一起来看看Ruby中的对象模型实现。
Class
class Baby
def say
puts "hello world!"
end
end
class 关键字来定义一个类,我们命名为Baby,同时我们定义个方法say,以Ruby的惯例,我们用Baby#say来表示这个方法,意指这是Baby类的实例方法。 所谓实例方法,就是一个类生成的实例对象才能响应的方法。
baby = Baby.new
baby.say #=> "hello world!"
baby.class #=> Baby
我们创建一个baby对象,baby响应了一个class消息,返回了baby的类。class消息,说的更通俗一些,就是class方法,是Ruby内建的方法,帮助你了解一个对象是来自于哪个类。
「Ruby中一切皆对象」,念念不忘,必有回想。
既然这样,Baby类难道也是一个对象?那么让我试试看Baby类能否响应class方法:
Baby.class #=> Class
Baby类响应了class方法,返回了一个类:Class
让我们继续思考:
Class.class #=> Class
Class.class #=> Class
Class.class #=> Class”
Class响应了class方法,返回了Class类,不管你执行多少遍代码,返回的依旧是Class。
看到这里,你是不是明白了什么呢?
我们用一张图来表达:
这说明:Ruby中任意一个类,实际是Class的一个实例对象。Class本身也是自己的一个实例对象。
klass = Class.new #=> <Class:0x007ff03ad71220>
klass.class #=> Class
看到这里,不知道有没有人和我有一样的疑问,那个class方法,是在哪里定义的呢?好神奇。
class Baby
end
但是Baby类,我们并没有继承任何类。我们回头看上文中的代码:
Baby.class #=> Class
Baby的类是Class,那么这个实例方法,应该是被定义在Class中的了。只有这样,Ruby中的任意类,才可以响应class方法。
SuperClass
让我们继续了解关于Class秘密。
Ruby是正宗的OOP语言,OOP的一个典型的特征是继承。
class Father; end
class Baby < Father; end
上例中,Baby类继承于Father类,Baby类叫子类,Father类叫父类(也叫超类)。Ruby中用小于号(<)来实现继承。
Ruby中既然有方法class可以让我们了解一个对象的类,应该也有方法让我们了解一个类的父类(超类),那就是superclass方法。
Baby.superclass #=> Father
Father.superclass #=> Object
Object.superclass #=> BasicObject
BasicObject.superclass #=> nil
我们可以明显的看出来这样一个继承链:
Baby < Father < Object < BasicObject
nil是Ruby中表示空对象的一个值,nil.class是NilClass。BasicObject响应superclass方法返回nil,说明BasicObject已经没有父类(超类)类,BasicObject已经是顶层类了。
当然,这些类也必然都是Class的对象了。
Object.class #=> Class
BasicObject.class #=> Class
那么Class有没有父类呢? 好奇的我,决定试试:
Class.superclass #=> Module
Module? 让我们继续:
Module.superclass #=> Object
Object.superclass #=> BasicObject
又回到原点了。
用一张图来表示:
其实Ruby中还提供一个方法,可以让我们清晰的看到一个类的继承链。
Baby.ancestors
#=> [Baby, Father, Object, Kernel, BasicObject]
ancestors,英文单词有「祖先」的意思,所以这个方法顾名思义,会返回Baby类的继承体系祖先链。基本和我们上面列的继承链是一样的,但是我们看到多出一个Kernel来。
Module
让我们看看Kernel是个是什么?用class方法试试,看它返回什么信息。
Kernel.class #=> Module
原来它是Module。Module我们之前见过,它本身也是一个类,同时是Class类的超类。那么Module到底是什么呢?有什么用?
其实,Ruby中除了正常的类之外,还有一种特殊的类叫做模块。
module Behavior; end
用module关键字定义的特殊类,就叫做模块。
Behavior.class #=> Module
可以看得出来,模块是Module的实例对象。
综合前文所说,可以做一个总结:
类,是Class的实例对象。 模块,是Module的实例对象。 注意我们的描述: 英文「Module」,是指Module类。 中文「模块」,是指Module类的实例对象。
「模块」是一种特殊的类,那么它特殊在哪里呢?让我用类的概念来试探一下:
behavior = Behavior.new
#=> NoMethodError: undefined method `new' for Behavior:Module
当我们给Behavior模块发送new消息的时候,报错。错误信息很明显:「模块Behavior没有定义new方法」, 意思就是模块无法响应new方法,而我们知道,new方法是类用来创建实例对象的,所以,模块的一个特性就被我们了解了,那就是:模块无法创建实例对象。
有的人可能就纳闷了,一个无法创建实例对象的类有什么用?
Mixin
再回到OOP的概念里,面向对象除了继承的特性之外,还有多态。
可能学过Java的朋友会想,Ruby是弱类型动态语言,没有接口,没有抽象类,怎么实现多态呢?
请忘记你学的Java吧。多态是面向对象语言的特性,Ruby中当然有,只是实现方式不同而已。 Java中用接口实现多态的方式,只是Java自己的实现,不要用Java的思维来思考Ruby。这也是很多从Java转向Ruby程序员的通病。
Ruby中实现多态是利用一种叫Duck Typing的特性,中文翻译叫「鸭子类型」, 意指:在Ruby这种弱类型语言中,我们应该关注一个对象的行为,而不是它的类型。就好像哪怕是一只「鸡」,叫声如果像鸭子,那么就可以把它当鸭子对待。
Duck Typing是Ruby的精髓。
class Duck
def cry
"ga...ga...ga"
end
end
class Chicken
def cry
"gua ... gua ... gua"
end
end
class Person
def kill(fowl)
fowl.cry
end
end
person = Person.new
duck = Duck.new
chicken = Chicken.new
person.kill duck #=> "ga...ga...ga"
person.kill chicken #=> "gua ... gua ... gua"
上面代码中,Duck和Chicken都属于家禽的一种,某个人person,宰杀鸭子和宰杀鸡的时候,它们都会cry,叫声还差不多。
这就叫多态。
我们可以看到上面的代码,Duck和Chicken两个类都实现了cry方法,如果再有更多的类,我们需要实现更多的cry方法,而面向对象编程的初衷就是,为了方便代码复用,这样做显然不科学。
这时候模块就该出场了。
模块有一个特性,叫做Mixin,中文翻译为「混入」。Mixin本质上可以理解为Ruby实现多继承的一种方式,不管你有多少个模块,mixin到一个类里, 这些模块就会「挂」到这个类的祖先链(单继承树)上面。
module Behavior
def cry
"ga ga ga"
end
end
class Duck
include Behavior
end
class Chicken
include Behavior
end
我们使用include方法,就可以把模块Behavior的代码混入到Duck类和Chicken类中,让Duck和Chicken都可以响应cry方法。这样代码就达到了复用的效果。
但是,这样做很明显的,Duck和Chicken的cry完全一样了,这样还不是很科学。
让我们继续改造。
封装
OOP还有一个重要的特性:封装。
所谓封装,就是把对象的属性和行为封装为一个独立的单位,尽可能隐藏细节。
回到前文中的代码示例,对于Duck和Chicken而言,cry是一种行为,cry的声音是一种属性。
class Duck
attr_accessor :sound
def initialize
@sound = "ga...ga...ga"
end
end
class Chicken
attr_accessor :sound
def initialize
@sound = "gua...gua...gua"
end
end
duck = Duck.new
duck.sound #=> "ga...ga...ga"
chicken = Chicken.new
chicken.sound #=> "gua...gua...gua"
这样,我们就实现了属性的封装。然后修改我们的模块Behavior, 完整代码如下:
module Behavior
def cry
sound
end
end
class Duck
include Behavior
attr_accessor :sound
def initialize
@sound = "ga...ga...ga"
end
end
class Chicken
include Behavior
attr_accessor :sound
def initialize
@sound = "gua...gua...gua"
end
end
duck = Duck.new
duck.cry #=> "ga...ga...ga"
chicken = Chicken.new
chicken.cry #=> "gua...gua...gua"
这样,就优雅的实现了我们的需求。
类方法
以上,我们一直都在说的是,对象间的通信。 那么类之间该如何通信呢?或者说,怎么样给类发送一个消息?
可能有人会想,类也是个对象嘛。
恭喜你,猜对了。
给类发送消息,和给对象发送消息是一样的。
class Person
def self.eat(food)
puts food
end
end
Person.eat "meat" #=> "meat"
用我们常用的「.」给它发消息就可以了,只要它能响应,它就会返回结果。如果不能响应,它则会报错。
但是细心的人注意到了,这个地方定义的方法和上面的不一样:
def self.eat
这里多了一个self。 self,顾名思义,是自己的意思。 你在一个类里,定义个方法,指明了这个消息接收者是self, 那就是说,这个消息是自己接收的, 这里的「自己」就是指Person Class。所以,这样定义的方法叫类方法。
可能有的人要问了, 你上面定义的实例方法,也没指定接收者啊。
是的,我的确没有指定一个「显式」的接收者。但是还有一个「隐式」的接收者。
class Person
def eat(food)
puts food
end
end
scope
Ruby中的class、def、module关键字,实际上定义了各自的作用域,也就是说,它们有各自的地盘。
class Person
puts self
def eat
puts self
end
def self.sing
puts self
end
end
#=> Person
Person.sing #=> Person
Person.eat #=> NoMethodError: undefined method `eat' for Person:Class
Person.new.eat #=> #<Person:0x007febd4a38cd0>
打印不同的scope的self,我们可以了解,class、def都定义了自己的域,并相互隔离。
class定义的是只有类才能访问的地盘。 def 一般定义的scope是只有实例对象才能访问的地盘,也就是实例方法。 而def如果在类地盘上指定了显式的接收者self,这时候self是指类本身,所以定义的是一个类方法。
加显式self的时候,一定要注意self是在哪一个scope里。
而隐式self,实际就是那个默认可以响应这个scope里消息的接收者。
比如上例中的eat方法,默认的scope是实例对象的,所以它是只能实例对象响应的方法。
SingletonClass
定义类方法,还有一种写法:
class Person
class << self
def eat(food)
puts food
end
end
end
Person.eat('meat') #=> "meat"
注意,我们这里是在 class << self ... end 这样一个域里面定义的方法。
好吧,告诉你真相:
class Person
class << self
puts self
end
end
#=> #<Class:Person>
这里打印了class << self , 这个self是指#
我们再来看看这到底是个什么东东。
class Person
@person = class << self
self
end
def self.born
puts @person.new
end
end
Person.born
#=> TypeError: can't create instance of singleton class
当我给Person发送born消息,返回一个错误,翻译过来就是: 类型错误: 不能给一个singleton class创建实例。
噢,原来这个 class << self 是singleton class。
我们继续探索:
class Person
@person = class << self
self
end
def self.born
puts @person.class
puts @person.superclass
end
end
Person.born
#=> Class
#=> #<Class:Object>
我们可以看到, class << self这个singleton class,它还是Class的对象(当然,每个类都是Class的对象),但是它的父类(超类)是一个奇怪的
#<Class:Object>
, 和刚才的
#<Class:Person>
格式一样,看来,这个
#<Class:Object>
也是一个Singleton Class。 而正常的类Person的superclass,是Object。
class Object
class << self
puts self
end
end
#=> #<Class:Object>
我们把Object类的Singleton Class 打印出来(注:因为Ruby的开放类特性,这里不是重新定义Object类),看到也是
#<Class:Object>
那么这俩是同一个吗? 我们继续代码验证:
class Object
class << self
puts self.object_id
end
end
#=> 70108374406880
class Person
@person = class << self
self
end
def self.born
puts @person.class
puts @person.superclass.object_id
end
end
Person.born
#=>Class
#=> 70108374406880
可以看得出来,打印了两个singleton class的object_id ,都是70108374406880,足以证明,他们是同一个对象了。
那么说了这么多,singleton class和类方法有什么关系呢?
现在回想一下, 我们创建普通类的实例方法,是在普通类开辟了一个实例对象的地盘(scope),而定义类方法, 我们实际是开辟了一块类的私有区域(scope),也就是类的singleton class。
class Person
def self.born
puts "baby"
end
class << self
def eat(food)
puts food
end
end
end
Person.born #=> "baby"
Person.eat("meat") #=> "meat"
Person.singleton_methods #=> [:born, :eat]
可以知道,def self.xxx 这种形式,实际上也是开辟了一个singleton class域,方法定义于这个私有区域里面,供类来使用。
当然了,我们能指定显式接收者是self,也可以指定其他对象,比如:
class Person; end
person1 = Person.new
person2 = Person.new
def person1.eat(food)
puts food
end
person1.eat("meat") #=> "meat"
person2.eat("meat") #=> NoMethodError: undefined method `eat' for #<Person:0x007f86c1a87e48>
上例中,如果把一个实例对象作为接收者定义一个方法,那么这个方法就只能响应这个对象person1, 而person2对象无法响应这个方法,所以报错。
其实每个实例对象,也有自己的singleton class:
class Person; end
person1 = Person.new #=> #<Person:0x007f86c21e3458>
class << person1
puts self
end
#=> #<Class:#<Person:0x007f86c21e3458>>
可以看得出来,这个singleton class是那个person1实例对象的singleton class。
我们可以叫它「单类」。
它只是跟随每个正常的类,包括Class类自己,都有一个单类的存在:「#」,你可以自己写代码去验证一下。
所以,到现在为止,我们应该明白了,什么是singleton class。
singleton class就是存在于Ruby中每个对象中,和对象形影不离,而实例对象是自己本身的一个隐藏类。
Ruby在1.9版本之后,也提供给我们一个很方便的内建方法,可以方便的使用单类:
class Person
puts singleton_class
end
Ruby中每个对象都有自己的Singleton Class这样一个单类。
Ruby的实例方法被定义在普通类中, 而Ruby的类方法,被定义在它的单类(Singleton Class)中, 所谓的类方法,实际上是一个类的专属方法。
Ruby中的class、def、module关键字,会定义一个scope。scope之间是相互隔离的。
示例,这段代码:
class Person
def eat
puts "eat food"
end
def self.sing
puts 'song'
end
class << self
def born
puts "baby"
end
end
end
用linux文件系统类比就是下面这样:
person_class/
|_ .singleton_class /
| |_ sing
| |_ born
|_ eat
方法查找
到目前为止,我们大概了解了Ruby的对象模型,那么在这种对象模型之下,消息该怎么传递呢。
类之间又有继承,又有模块的Mixin,我们前面讲过,Mixin实为多继承。看示例:
class A
def t
puts 'in a'
end
end
module B
def t
puts "in b"
end
end
class C < A
include B
def t
puts 'in c'
end
end
c = C.new
c.t
面对这样的代码,你知道c.t 响应的是哪个方法吗?
你可能会说,C里面定义了t方法,当然会输出「in c」啊。
那么咱们把C里面的t方法去掉,你知道c.t会响应哪个方法吗?
如果想不通,就让我教你一个最简单的方法:
C.ancestors
#=> [C, B, A, Object, Kernel, BasicObject]
C的ancestors方法,可以返回C类的祖先链,也就是一个继承树。
Ruby是单继承,Ruby之父为了简化多继承,用Mixin的方式,把模块也挂载到这样一个单继承树上面,所以我们一目了然C的所有继承关系。
而Ruby中的消息传递,是沿着这个继承树往上查找的。
比如c.t这个消息,如果C类中没有定义这个方法,那么就会去B里面找,B里定义了t方法,就会响应,从而输出「in b」。如果B里没有定义t方法,就会找到A,直到继承树顶端。
上面只是没有考虑singleton class的情况,如果我们的代码中包含了singleton class,那么方法查找的顺序就会有所改变:
class Person
def eat
puts "food"
end
def say
puts "hello"
end
end
person = Person.new
class << person
def eat
puts "meat"
end
end
上面这个例子,当你执行person.eat的时候,该是哪个方法会被响应?person.say呢?
事实证明,person.eat输出的是"meat",所以方法查找会先从singleton class开始,如果找不到,会“沿着这个singleton class的父类(超类)往上查找,当你执行person.say的时候,就会到Person这个类中去查找。 这是因为person.singleton_class.superclass 就是Person。
总结
以上,就是Ruby的对象模型。如果你理解了Ruby的对象模型,那么Ruby的代码,基本就可以在你脑中运行了。