类,对象和变量
看了前面我们谈论到的一些例子,你也许会怀疑ruby的面向对象特性是否属实,这章我们将会详细讲述这方面的内容。我们将会探讨在ruby中如何创建类和对象,并且讨论ruby比其他面向对象语言的一些更强之处。同时,我们也会部分实现我们数亿美元的产品:基于因特网的爵士和布鲁斯自动点唱机。
经过几个月的工作,我们负责的研究人员决定我们的点唱机学要歌曲(songs),所以我们要在ruby中建立一个song类来表示现实中的歌曲。我们知道歌曲都有一个名字,演唱者,时长等,所以,我们的song对象也应如此。
我们开始创建了一个类:Song
,[前面我们已经知道类名以大写字母开头,而方法以小写字母开头] 它只含有一个方法:initialize
.
class Song
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end
end
|
initialize
在Ruby中是一个特殊方当法,当你调用Song.new
来创建一个Song
对象的时候, Ruby先创建一个没有初始化的对象,然后调用它的initialize
方法,把传给new的参数再传给 initialize
这个方法。这样,我们就可以编写代码来设置对象的状态了。
对于类Song来说,i
nitialize
方法接收3个参数,这三个参数作用域跟方法里的局部变量一样,所以他们的命名方式更具备变量一样,以小写字母开头。
每个对象代表自己的歌曲,他们有不同的名字,演唱者和时长等,也就是我们要把这些东西当成对象里面的实例变量。在ruby中实例变量用@开头,比如我们上面的例子,@name,@artist,@duration都是实例变量。
让我们看看我们的成果如何:
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.inspect |
}} |
"#<Song:0x401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">" |
它已经可以工作了。默认的inspect
方法可以发送给任何对象,并且能得到这个对象的id和它的实例变量。从上面的结果看到我们正确的设置了对象的各个状态。
我们的经验告诉我们,在开发过程中,我们要多次打印Song中的内容,等inspect的默认格式不能完全满足我们的要求,幸运的是,Ruby有一个标准的消息to_s,当向一个对象发送这个消息时,将会返回一个字符串,比如对于song来说:
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.to_s |
}} |
"#<Song:0x401b499c>" |
这样没多少用处,甚至还不如inspect,只有对象id。但是我们可以重载这个to_s方法。同时,我们也会用一点时间来说说Ruby中如何定义一个类。
在Ruby中,类永远不会关闭,你可以一直往里面加入方法,不光是你自己写的类,系统内建的类也可以加入的。你需要做的是打开一个类的定义,然后就可以加入自己的方法了。
这对我们来说非常好。在本章以后的例子里,我们只需要添加新的方法,老的方法还继续存在,这样省得我们花费多余的时间去在每个例子里都重写一遍。尽管我们现在写的代码比较分散,但最好还是把它们都写到一个文件中去比较好。
我想已足够详细了,还是回到我们要添加的to_s方法吧。
class Song |
def to_s |
"Song: #{@name}--#{@artist} (#{@duration})" |
end |
end |
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.to_s |
}} |
"Song: Bicylops--Fleck (260)" |
非常好,我们进步了不少。但是你也许觉得被骗了,我们曾经说过Ruby中所有对象都支持to_s方法,但没有说怎么支持,答案是继承。ruby如何决定当一个对象接受一个消息后执行哪个方法,在下一节我们将会看到。
继承使你能够创建一个基于一个类的特殊化的类,比如,我们的自动点唱机里有song这个类,但是随着市场的需求,我们需要增加对卡拉ok的支持。卡拉ok也是歌曲的一种,只不过光有伴奏,没有演唱音,但是他们需要歌词这个属性,当我们播放卡拉ok时,歌词还要显示出来。
比较好的办法是定义一个类KaraokeSong
,它就是一个song,但是有一个歌词的属性。
class KaraokeSong < Song
def initialize(name, artist, duration, lyrics)
super(name, artist, duration)
@lyrics = lyrics
end
end
|
" < Song" 告诉 ruby karaokeSong 这个类是Song的一个子类,Song是karaokeSong的父类。先不用管initialize
这个方法,以后我们会谈到。
我们可以创建KaraokeSong
对象看看它是否能工作(在最后的系统中,lyrics存在另外的对象里,这个对象有文本和时间信息。为了测试方便,我们这里只用了字符串。这也是无类型语言的优点:我们在执行代码之前不需要对所有对象进行定义。
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...") |
aSong.to_s |
}} |
"Song: My Way--Sinatra (225)" |
这个类已经可以工作了,但是to_s没有显示歌词信息。
这和ruby如何决定调用哪个方法的机制有关。当ruby看到这个 aSong.to_s
方法调用,它并不需要知道去哪里找to_s
这个方法,而是要在程序运行到此的时候再去调用这个函数。开始在aSong
里面找,如果这个类里面定义了一个和发送给这个对象的消息一样名称的方法的话,就运行这个方法。否则,就会到这个类的父类去找,如果还没找到,再到父类的父类去找。这样一直找到祖先Object。如果找到最高层还没有找到这个方法,一般会返回一个错误。[实际上,你可以拦截这个错误,你可以在运行时弥补这个错误,见 Object#method_missing
]
现在再回到我们的例子,我们向aSong发送了一个消息to_s,在karaokeSong这个类里,ruby找不到to_s这个方法,所以,再去karaokeSong的父类Song去找。在父类里发现了这个方法,所以就执行这个方法,所以,它只打印了除了歌词的信息。类Song一点都不知道lyrics的存在。
我们可以在这里实现这个方法来弥补这个不足,又很多方法可以实现这个方法,我们先来看一下一个不是很好的例子,从Song的to_s拷贝出来一些代码,然后加上lyric信息。
class KaraokeSong |
# ... |
def to_s |
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]" |
end |
end |
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...") |
aSong.to_s |
}} |
"KS: My Way--Sinatra (225) [And now, the...]" |
我们正确地显示了@lyrics
这个实例变量,但是这样做直接在子类里访问了父类的实例变量,为什么这样实现to_s方法不好呢?
这和好的编程风格有关(可以称作decoupling)
The answer has to do with good programming style (and something called ). By poking around in our parent's internal state, we're tying ourselves tightly to its implementation. Say we decided to change Song
to store the duration in milliseconds. Suddenly, KaraokeSong
would start reporting ridiculous values. The idea of a karaoke version of ``My Way'' that lasts for 3750 minutes is just too frightening to consider.
我们需要每个类只操作自己内部的状态,当KaraokeSong#to_s
被调用的时候,先在KaraokeSong#to_s
调用父类的to_s方法,然后在加上lyric信息返回给调用者。这里需要ruby的关键字"super"。当你不带参数调用super时,ruby向父类发送消息,调用父类的同名函数(即和子类同名的函数),传递给当前类方法的参数会默认的传给父类。比如改写后如下:
class KaraokeSong < Song |
# Format ourselves as a string by appending |
# our lyrics to our parent's #to_s value. |
def to_s |
super + " [#{@lyrics}]" |
end |
end |
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...") |
aSong.to_s |
}} |
"Song: My Way--Sinatra (225) [And now, the...]" |
我们显示的声明了KaraokeSong
是Song的一个子类,但是并没有说明Song的父类。如果定义一个类时没有指定父类,默认为Object为它的父类。也就是说,所有的类的祖先都是Object类,而且Object的实例方法在子类中也是可以访问的。比如to_s是ruby中大概35个实例方法之一,这些方法列表后面可以看到。
像c++这样的面向对象语言都支持多重继承,也就是说一个类可以有多个父类,从每个类继承特性。尽管很有效,但它有时候很危险,有可能产生混乱。
其他一些语言,比如java,支持单继承,一个类只能有一个父类,尽管清洗明了,容易实现,但是也有一些缺点,因为事实上一个事务同时具备很多种事务的特征。比如一个球,既是球形的东西,也是能弹跳的东西。
ruby采取了有趣而强大的折中办法,你能轻松的实现单继承和多继承。一个ruby只能有一个直接父类,是单继承语言,但是ruby类可以包含其他的mixin(mixin可以看作是一个部分类定义a partial class definition)中的一些功能,从而引入附加的功能,以这种方式实现了多重继承,并且不会出现多继承语言中的问题。
上面我们已经看到了类和方法,下面来看看对象,也就是类的实例。
Song
对象有一些内部属性,比如名称和演唱者,这些属性都是私有的,其他对象都不能直接访问。一般来说,这样是不错的设计,每个对象只负责自己的完整性,一致性。
但是,如果把对象装饰的这么秘密将会使这些对象变得毫无作用,我们能创建它,但是我们不能修改它的属性。所以,我们可以定义一些方法,通过这些方法,外部对象可以访问,修改对象的属性。这些从外面看起来表现叫做属性(attributes)。
对于我们Song对象,我们可能需要访问它的名字和演唱者,以便在播放的时候打印出来,还有它的时长(可以用类似进度条来显示)。
class Song |
def name |
@name |
end |
def artist |
@artist |
end |
def duration |
@duration |
end |
end |
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.artist |
}} |
"Fleck" |
aSong.name |
}} |
"Bicylops" |
aSong.duration |
}} |
260 |
这里,我们定义了三个访问方法,每个方法返回一个实例属性。在实际中,这些操作很普遍,所以ruby提供了一个方便的方法:用attr_reader,它将为我们自动创建访问方法。
class Song |
attr_reader :name, :artist, :duration |
end |
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.artist |
}} |
"Fleck" |
aSong.name |
}} |
"Bicylops" |
aSong.duration |
}} |
260 |
这个例子引入了一些新东西,比如 ":artist"可以当作一个表达式,返回一个指向artist的符号链接。也可以把":artist"当作是artist的名字。这个例子里,我们定义了三个访问方法: name
, artist
, duration
。而实例变量@name
, @artist
, @duration
会自动创建。这样定义访问方法和我们上面写的一样。
有时候需要在外部对对象的属性进行修改。比如,一首歌的时长这个属性可能开始的时候只是一个估算的值,当第一次播放的时候,我们知道了它的真正时长,并且要把它写回到Song这个对象。
在c++或者java中,我们可以用setter方法。
class JavaSong { // Java code
private Duration myDuration;
public void setDuration(Duration newDuration) {
myDuration = newDuration;
}
}
s = new Song(....)
s.setDuration(length)
|
在Ruby中,可以象其他变量一样访问属性,比如上面我们调用了aSong.name
,所以我们也应该像变量一样给属性赋值。在ruby中,这样做就行:
class Song |
def duration=(newDuration) |
@duration = newDuration |
end |
end |
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.duration |
}} |
260 |
aSong.duration = 257 # set attribute with updated value |
aSong.duration |
}} |
257 |
赋值语句"aSong.duration = 257
"调用了aSong中的方法duration=
参数为257
。实际上,一个方法名以=结尾,就像这个属性出现左边的赋值语句一样。同样,ruby也为创建可写属性提供了一个快捷方式
class Song
attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257
|
这些属性访问方法不是对一个对象的实例变量的包装,比如,你需要得到以分钟为单位的时长,而不是以秒为单位:
class Song |
def durationInMinutes |
@duration/60.0 # force floating point |
end |
def durationInMinutes=(value) |
@duration = (value*60).to_i |
end |
end |
aSong = Song.new("Bicylops", "Fleck", 260) |
aSong.durationInMinutes |
}} |
4.333333333 |
aSong.durationInMinutes = 4.2 |
aSong.duration |
}} |
252 |
这里我们用属性方法建立了一个虚拟的实例变量,对于外面来说durationInMinutes
可以看作和其他一样的属性,但实际上,并没有与之对应的实例变量。
这并不止是有趣而已,在Bertrand Meyer 的杰作《Object-Oriented Software Construction》 中,作者称这叫做统一访问原则(Uniform Access Principle)。通过把这些实例变量和他们计算之后的值隐藏起来,你就可以不用在自己的实现里来处理这些问题,而且,当需要改动的时候,你只需要改动一个文件,而不是很多文件。
到目前为止我们讨论的都是实例变量和实例方法,这些变量术语每个不同的对象,用方法来操作,有时候,类也可能需要自己的状态,所以引入了类变量。
一个类变量被所有它的对象实例共享,也可以被下面要提到的类方法修改。在系统中,类变量只有一个拷贝。类变量名以两个at 即"@@"开头,比如"@@count"。不想全局变量和实例变量,类变量在使用之前必须被初始化。通常初始化只是类定一中的一条赋值语句。
比如,在我们的自动点唱机中,我们想记录一个指定的歌曲播放过多少次,这个次数应该是一个song实例变量,每当这个Song被播放,这个变量都要加1。如果,我们还想计算所有歌曲总共播放了多少次,我们可以找到所有Song对象,然后累加他们的播放次数,或者用全局变量,相反,这里我们用了类变量。
class Song
@@plays = 0
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
@plays = 0
end
def play
@plays += 1
@@plays += 1
"This song: #@plays plays. Total #@@plays plays."
end
end
|
为了调试方便, Song#play
方法返回了一个字符串,显示了这首歌播放过多少次,和所有歌曲的总的播放次数。
s1 = Song.new("Song1", "Artist1", 234) # test songs.. |
s2 = Song.new("Song2", "Artist2", 345) |
s1.play |
}} |
"This song: 1 plays. Total 1 plays." |
s2.play |
}} |
"This song: 1 plays. Total 2 plays." |
s1.play |
}} |
"This song: 2 plays. Total 3 plays." |
s1.play |
}} |
"This song: 3 plays. Total 4 plays." |
类变量属于类和它的实例私有,如果你想在外面访问它,需要编写访问方法,既可以是实例的访问方法,也可以是下面我们要说到的类的访问方法。
有时候,一个类需要提供一个不需要任何类实例就能使用的方法。我们已经见过一个这样的方法了,new方法创建了一个Song对象,但是它不属于Song类。
你将会发现类方法贯穿于ruby的库文件之中。比如,File类的对象表示一个打开的文件,但是File也提供了几个类方法,比如删除文件,我们不需要打开文件,直接调用 File.delete
,提供要删除的文件名就行了。
File.delete("doomedFile")
|
类方法和实例方法定义时候是不一样的,类方法定义的时候要加上类名:
class Example
def instMeth # instance method
end
def Example.classMeth # class method
end
end
|
我们的自动点唱机是要收钱的,按歌曲数量而不是时间来收,所以提供长的歌曲不如提供短的歌曲效益高。我们不希望在SongList
中出现太长的歌曲,所以我们在SongList里面定一个类方法,判断一首歌是否超过了规定的长度。这个长度存在一个常量里面(常量以大写字母开头),并且在类体里面初始化这个常量。
class SongList |
MaxTime = 5*60 # 5 minutes |
|
def SongList.isTooLong(aSong) |
return aSong.duration > MaxTime |
end |
end |
song1 = Song.new("Bicylops", "Fleck", 260) |
SongList.isTooLong(song1) |
}} |
false |
song2 = Song.new("The Calling", "Santana", 468) |
SongList.isTooLong(song2) |
}} |
true |
有时候,你想改变缺省的对象的创建方式,比如,对于我们的点唱机系统,我们又很多点唱机,遍布全国,我们想尽可能的使他容易维护,所以我们需要记录点唱机发生的所有事情,比如一首歌被播放了,收钱了等,所以我们需要一个日志类。因为我们想把带宽留给音乐数据,所以日志记录在本机。我们想一个点唱机系统只有一个log类,并被系统中的所有类共有使用。
通过使用单例模式,要想使用这个log类,只有一种创建方法:Logger.create
,并且确保系统中只有一个log的实例存在。
class Logger
private_class_method :new
@@logger = nil
def Logger.create
@@logger = new unless @@logger
@@logger
end
end
|
我们把logger类的new方法设成了私有的,这样就不能用Looger.new来创建logger对象了。我们提供了一个类方法 Logger.create
,用到了类变量 @@logger
,这是一个指向logger类实例的引用。可以看到,如果实例已经创建了,这个方法直接返回已经创建的实例,不会再创建第二个。[这里的实现是非线程安全的,如果有多个线程来访问这个函数,可能会出产生多个logger对象。我们可以用ruby提供的Singleton
mixin来解决,而不必自己处理线程安全问题。]我们可以检查一下这两个方法的返回情况。
Logger.create.id |
}} |
537766930 |
Logger.create.id |
}} |
537766930 |
用类方法来包装构造函数,也可以让使用你的类的人感到轻松。比如我们的类Shape代表一个多边形,构造函数接收边数和周长:
class Shape
def initialize(numSides, perimeter)
# ...
end
end
|
但是,多年以后,使用方法变了,现在需要提供shape的名称,边数,和边长而不是周长。而我们只需要加几个类方法就行了:
class Shape
def Shape.triangle(sideLength)
Shape.new(3, sideLength*3)
end
def Shape.square(sideLength)
Shape.new(4, sideLength*4)
end
end
|
类方法还有很多强大有趣的特性,但是目前我们还是要继续我们现在的内容。
我们设计一个类的接口的时候,一个重要的问题是,我们应该向外界暴露多少内部实现,外部能访问我们的类有多少限制。如果过多的让外部访问内部的东西,可能增加了耦合,用户越来越依赖我们的类的内部实现,而不是逻辑接口。因为我们要改变一个实例的状态需要调用这个实例的相关方法,控制对实例的方法的访问,就能避免对对象实例的状态的直接修改。Ruby提供了三种保护层次:
- Public methods 任何人都可以访问,没有访问控制。方法默认都是public(
initialize
除外)。
- Protected methods 可以在本类或者子类中调用。访问控制在家族内。
- Private methods 不能用显示的接收者来调用。cannot be called with an explicit receiver. Because you cannot specify an object when using them, private methods can be called only in the defining class and by direct descendents within that same object.
"protected"和"private"两者的区别非常微妙,在ruby中,两者的关系和在其他语言中是不一样的。如果一个方法是protected的,它可以在定义它的实例或者子类的实例来调用。如果一个方法是"private"的,只可以在这个方法所处的对象中被使用,不能直接调用另一个对象的private方法。甚至这个对象就是调用者本身。
Ruby和其他oo语言另一个重要的不同点在于,ruby动态确定访问控制,在程序运行而不是静止时,只有你运行到那一行,才会去判断是否出错。
在一个类或者模块定义中设定方法的访问控制层次: public
, protected
,private
。有两种定义方法。
如果不带参数使用 public/protected/private,那么这后面的方法默认都是指定的值,比如一行写了private,那么后面的方法默认都是private,除非指定了另外的访问控制符。
class MyClass
def method1 # default is 'public'
#...
end
protected # 后面方法将是 'protected'
def method2 # will be 'protected'
#...
end
private # 后面方法将是 'private'
def method3 # will be 'private'
#...
end
public # subsequent methods will be 'public'
def method4 # and this will be 'public'
#...
end
end
|
另一种方法,定义方法的时候不指定访问控制符,而是将相关的方法列在对应的访问控制后面,比如:
class MyClass
def method1
end
# ... and so on
public :method1, :method4
protected :method2
private :method3
end
|
initialize
方法自动声明为private型。
现在来看看一个例子。假如我们有一个记帐系统,每一个借方(debit)对应一个贷方(credit),我们要求都必须遵守这个规则,所以我们把debit和credit的方法设成private,提供了一个外部接口来处理:
class Accounts
private
def debit(account, amount)
account.balance -= amount
end
def credit(account, amount)
account.balance += amount
end
public
#...
def transferToSavings(amount)
debit(@checking, amount)
credit(@savings, amount)
end
#...
end
|
Protected access is used when objects need to access the internal state of other objects of the same class. For example, we may want to allow the individual Account
objects to compare their raw balances, but may want to hide those balances from the rest of the world (perhaps because we present them in a different form).
class Account
attr_reader :balance # accessor method 'balance'
protected :balance # and make it protected
def greaterBalanceThan(other)
return @balance > other.balance
end
end
|
Because the attribute balance
is protected, it's available only within Account
objects.
变量用来跟踪一个对象的状态,是指向其他对象的一个引用 。
person = "Tim" |
person.id |
}} |
537771100 |
person.type |
}} |
String |
person |
}} |
"Tim" |
第一行,我们创建了一个新字符串对象"Tim",person是指向这个字符串对象的一个引用,下两行的测试语句显示了这个对象的类型和id,最后显示了它的值。
但变量是对象吗?
在ruby中,答案是否定的。一个变量只是指向一个对象的引用。这些对象可能正在某地,比如堆栈中,变量只是指向了这些对象。
我们再看看稍微负责的例子
person1 = "Tim" |
person2 = person1 |
|
person1[0] = 'J' |
|
person1 |
}} |
"Jim" |
person2 |
}} |
"Jim" |
我们改变了person1的第一个字符,但是第二个也跟着改了。这是因为变量只是指向一个对象的引用,而不是对象本身。赋值语句person2 = person1
没有创建新的对象,只是person2拷贝了person1的引用而已。所以person1和person2都指向了同一个对象。
有时候赋值语句只是为对象建立了别名,而潜在的创建了新的变量指向同一个对象。这在我们的代码中可能会引起问题,但并不像你想的那么容易出错。我们可以用String的dup方法,这个方法会创建一个新的字符串对象,并且内容和消息接受者(方法执行者)一样。
person1 = "Tim" |
person2 = person1.dup |
person1[0] = "J" |
person1 |
}} |
"Jim" |
person2 |
}} |
"Tim" |
如果你不想别人修改一个对象,也可以冻结(freezing)这个对象。如果你修改一个被冻结的对象,ruby会抛出一个TypeError
异常。
person1 = "Tim"
person2 = person1
person1.freeze # prevent modifications to the object
person2[0] = "J"
|
produces:
prog.rb:4:in `=': can't modify frozen string (TypeError)
from prog.rb:4
|
Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2001 by Addison Wesley Longman, Inc. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/)).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.