Ruby-类,对象,变量

来自站长百科
跳转至: 导航、​ 搜索

导航: 上一页 | ASP | PHP | JSP | HTML | CSS | XHTML | aJAX | Ruby | JAVA | XML | Python | ColdFusion

继承和消息[ ]

继承允许你创建一个类,这个类是另一个类的改进版或是特殊版。比如说我们的点唱机有歌曲的概念,我们把它抽象成Song类,后来,市场发展了,我们被告知需要提供对卡拉OK的支持,一支卡拉OK歌曲本质上还是一首歌(它没有歌声,不过这和我们没什么关系),不过它多了和时间同步的歌词,当我们的点唱机演奏一首卡拉OK时,歌词应该随着音乐的时间在屏幕上滚动过去。

一个解决办法是定义一个新类,KaraokeSong,就像Song类,不过多了歌词。

class KaraokeSong < Song
def initialize(name, artist, duration, lyrics)
super(name, artist, duration)
@lyrics = lyrics
end
end


在这个类的定义中,"<Song"所在的行表示KaraokeSong是Song的子类。(不要惊奇,这就是说Song类是KaraokeSong类的父类。人们常常谈论父子关系,如此说来KaraokeSong的父亲就是Song了)。暂时不要管initialize方法,我们稍候再讨论super的用法。

我们来创建一个KaraokeSong对象,检查我们代码的运行情况。(在目前的系统中,歌词存放在一个包含文本和时间信息的对象中,不过为了测试我们的代码,在这里我们使用字符串来存储,这是我们这种无类型语言的又一大优势----在我们运行代码前不必定义任何东西)。

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的真正位置,它忽略过去等到程序运行的时候再处理,到那时,Ruby首先在aSong的类中查找,如果它的类中实现了与这个消息同名的方法,这个方法就会被执行,否则,Ruby将在它的父类中寻找该方法,然后是祖父类…沿着族谱向上寻找,如果到最后都没有发现这个方法,Ruby将返回一个错误信息。(事实上,你可以截取这个错误信息,以便在运行时修正错误,355页的Object#method_missing有详细论述。)

现在回到我们的例子上。当我们给KaraokeSong类的对象aSong发送to_s信息时,Ruby首先在KaraokeSong类中寻找to_s方法,但没有找到,然后在其父类Song中寻找,结果找到了,to_s方法是我们在18页定义的。现在知道为什么to_s不显示歌词了,原来to_s根本不知道有歌词这回事。

我们完善一下代码来实现KaraokeSong#to_s,有好几种办法可以完成这项任务,我们先从一个不好的开始。我们从Song类中把to_s方法的代码拷贝过来,再加上歌词。

class KaraokeSong 
# ...
def to_s
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
end
end v aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s  ? "KS: My Way--Sinatra (225) [And now, the...]"


我们终于成功显示了@lyrics这个实例变量,为了这个目的子类直接访问了父类的实例变量,那么为什么说这不是个好办法呢?

这和好的编程风格(还有去耦)有关。我们在父类的内部闲逛,结果是我们的子类实现起来要和父类很紧密。如果我们把Song的持续播放时间改成用微秒做单位,那样KaraokeSong就会显出一些古怪的值,一首‘My Way’的卡拉OK持续要3750分钟,这会让人疯掉的。

每个类都应该只处理自身的内部状态,当KaraokeSong#to_s被调用的时候,我们让它调用父类的to_s方法来处理子类中的歌词细节,它会添加歌词信息然后返回结果,这里的诀窍是Ruby的关键字super。若你不带参数调用super,Ruby发送一个消息给当前类的父类,请求父类调用与当前方法同名的方法,并且把我们传递给当前方法的参数传递过去。现在让我们实现我们新的增强版to_s。

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...]"


我们明确地告诉Ruby,KaraokeSong是Song类的一个子类,但是我们没有指定Song自身的父类。如果在定义类的时候你不指定一个父类,Ruby就默认指定Object类为其父类(看到Java的影子了吗)。这意味着所有的类都把Object类当作是它们的祖先,Object类的实例方法基本上适用于Ruby的每个类。在18页我们说过to_s方法对所有的对象都适合,现在应该明白为什么了,因为to_s正是Object类中大约35个实例方法中的一个,Object类中的方法列表在351页。


继承和混合[ ]

一些面向对象语言(特别是C++)支持多重继承从而继承每个父类的功能,多重继承即一个类可以继承于多个直接父类。这种特性既强大又危险,因为它会使继承层次变得晦涩难懂。

其它一些语言,比如说Java,只提供了单继承,一个类仅有一个直接父类,虽然很简洁(也容易实现),但也存在缺点----在真实世界中,事物常常是从多个源继承得到属性(比如说,一个球既是弹跳的物体也是球形的物体)。

Ruby提供了一种巧妙而且功能强大的折衷办法,让你既可以拥有单继承的简练又可以拥有多重继承的强大。一个Ruby类只许有一个直接父类,这时Ruby是单继承的,同时Ruby又可以包含任意多个混合的功能(混合就像是类定义的一部分),这时Ruby实现了类似多重继承的功能而又没有什么缺陷。关于混合我们会在98页详细介绍。

目前为止,我们已经看到了类和它的方法,现在该看看对象了,比方说Song类的实例。


对象和属性[ ]

迄今为止我们所创建的Song对象都有一些内部状态(比如说歌曲标题和作者),对这些对象来说这些状态是私有的,就是说其它对象不可以访问该对象的实例变量。一般来说,这是一件好事,它意味着对象只须负责自己的安全。

但是,如果一个对象太过私有化就没有用处了----你可以创建它,但不能用它来做什么。为了让外部环境跟它交流,就要定义很多方法来访问和操作这个对象的状态,这些外在的特性叫做对象的属性。(译者注:这里属性指的是那些用来访问和操作对象状态的方法)

对于我们的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


在这里,我们定义了3个访问方法来返回3个实例属性的值。因为这种方法用的太普遍了,所以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的名字,而artist是变量的值。这个例子我们定义了三个访问方法: name, artist, duration,它们对应的实例变量@name, @artist, @duration同时被自动创建。这样定义的访问方法就和上面我们手写的是完全一样的。


可写属性[ ]

有时候你需要从对象外面来修改对象的属性。例如,假设Song类的播放持续时间最初只是一个估值(可能是从CD封面或者MP3的文件推测出来的),我们第一次播放歌曲时,得到歌曲的真实长度,然后把它回存到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的调用就可以看出,自然你也会想到,既然变量的值可以修改,那属性的值就也应该可以修改。遵循最小意外原则(参看[序]节的注1),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)。通过隐藏实例变量和实际计算值的不同,你就不必自己来实现这些处理,等到将来也可以很方便地修改它的工作方式同时又不用顾及成千上万使用你的类的文件了,这无疑是一场伟大的胜利。



类变量和类方法[ ]

到目前为止,我们创建的所有的类都包含了实例变量和实例方法:变量和类的一个特定实例关联,而方法操作这些变量。有时类自己也需要有自己的状态,因此引入了类变量的概念。


类变量[ ]


类变量在类的所有对象中是共享的(译者注:就像JAVA中类的static变量),并且可以被我们下文要提到的类方法访问。对于一个给定的类,特定的类变量只有一个拷贝。类变量的命名以两个"@"开头,就像"@@count",与全局变量和实例变量不同,类变量在使用前必须要初始化,这种初始化通常不过是类定义中一个简单的赋值语句而已。

举个例子来说,我们的点唱机可能需要统计每首歌曲被点唱的次数,这个次数可能是Song对象的一个实例变量,当歌曲播放的时候,这个变量的值就增加。我们同时也想知道被点唱的歌曲的总数目,我们可以搜索所有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对象,但和特定的歌曲没有什么关系。

aSong = Song.new(....)


你会发现在Ruby的库中充满了类方法,比如,File类实现在文件系统中打开文件,但是File类也提供了几个类方法用来操作文件,它们不需要打开文件所以也就没有File对象,例如你要删除文件,调用类方法File.delete,把文件名传递过去就可以了。

File.delete("doomedFile")


类方法与实例方法的定义方式不同。定义类方法要在方法名前加上类名和一个点号(".")。

class Example
def instMeth # 实例方法
end
 def Example.classMeth     # 类方法
end
end 


点唱机是按每首歌的点唱次数收费的,而不是按歌曲长度收费,所以短的曲目就比长的要有利可图。为了避免在歌单(SongList)中出现太长的歌,我们在SongList中定义一个类方法来检查歌曲是否超过某个限制,我们用一个类常量来设置这个限制,类中的一个简单的常量(还记得常量吗,大写字母开头..)。

class SongList 
MaxTime = 5*60 # 定义这个限制为5分钟
 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


单例与其它构造器[ ]

有时你可能会想要重写Ruby的默认构造方法,我们仍旧用点唱机来做例子。因为我们在全国各处有很多点唱机,于是就想要让维护工作变得简单一些。我们需要把在点唱机上发生的事件以日志形式全部记录下来,像被点唱的歌曲,收到的钱,可疑的流量等等,为了节约带宽,这些日志保存在本地,我们需要一个类来处理这些日志,但是,我们希望一个点唱机只有一个日志对象,还希望所有使用这个日志对象的其它对象共享它(译者注:意即所有对象只使用一个日志对象)。

通过使用《设计模式》中提到的单例模式,要做到上述这些要求就只有一种办法来创建日志对象,调用Logger.create方法,还要确保只有一个日志对象被创建。

class Logger
private_class_method :new
@@logger = nil
def Logger.create
@@logger = new unless @@logger
@@logger
end
end


把Logger的new方法声明为私有来防止其他人调用默认构造器创建日志对象,取而代之,我们提供了一个类方法Logger.create,它使用类变量@@logger来保持对一个单个的日志对象的实例的引用,每次调用这个方法时都返回这个实例。(我们这里介绍的单例的引用并不是线程安全的,如果多个线程同时运行,就有可能会创建多个日志对象。我们可以使用ruby提供的单例混合,而不必自己处理线程安全问题,在468页有详细描述。)我们可以查看方法返回的对象标识符来观察一下。

Logger.create.id >> 537766930
Logger.create.id >> 537766930


使用类方法来伪造构造器,也可以让那些使用你的类的人倍感轻松。举一个小例子,Shape类描述正多边形,创建Shape类的实例需要提供边数和总的周长给构造器。

class Shape
def initialize(numSides, perimeter)v # ...
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改变对象属性的唯一途径是调用对象的方法,这样,控制了对方法的访问,也就控制了对对象的访问。一个非常好的原则是永远不要把会使对象的属性值非法的方法暴露,Ruby提供了三种保护级别。

     公有方法pubic methods    可以被任何对象调用,不存在访问控制。方法默认都是公有的(initialize除外,它永远是私有的)。
保护方法protected methods 可以被定义它的类和其子类的对象访问,访问只限于家族内。
私有方法private methods 不能被外界调用,因为调用私有方法时无法指定对象,所以只能在定义它的类和类直接派生的对象中使用。

"protected"和"private"两者的区别非常微妙,在ruby中两者间的区别甚至要超出其它大多数面向对象语言。如果一个方法是保护的,它可以在定义它的类或者子类的实例中调用。如果一个方法是私有的,则只能在调用它的对象的上下文处调用,不可能直接调用另一个对象的私有方法,即便这个对象和该对象都是同一个类的实例。

Ruby和其他OO语言另一个重要的不同点在于,访问控制是动态确立的,就是说程序运行时,而不是静态的,只有你的代码执行受限的方法时才会得到访问违例。


设定访问控制[ ]

在一个类或模块的定义中你可以使用public,protected,private三种方式来设定方法的访问级别,它们的每一种又可以有两种不同的使用方式。

如果不带参数,这三种方式设置随后的方法为默认访问控制,这恐怕是尽人皆知的。如果你是C++或JAVA程序员,你会使用像public这样的关键字来达到同样的效果。

class MyClass
def method1 # 默认是 'public'
#...
end
protected # 随后的方法是 'protected'
def method2 # 这里是 'protected'
#...
end
private # 随后的方法是 'private'
def method3 # 这里是 'private'
#...
end
public # 随后的方法是 'public'
def method4 # 这里是 'public'
#...
end
end


另一种方式,把方法作为参数列在访问控制后面,来设置这些方法的访问级别。

class MyClass
def method1
end
# ... and so on
public :method1, :method4
protected :method2
private :method3
end


类的initialize方法自动被声明为私有的。

现在来看一些例子,假设我们在建模一个记帐系统,每一个借方对应一个贷方,因为我们想确保这个规则不被任何人打破,所以我们把处理借贷业务的方法私有化,然后我们按照事务处理的程序定义我们的外部借口。

class Accounts
private
def debit(account, amount)
account.balance -= amount
end
def credit(account, amount)
account.balance += amount
end
publicv #...
def transferToSavings(amount)
debit(@checking, amount)
credit(@savings, amount)
end
#...
end


保护访问用在当对象需要访问和它同一个类的其它对象的内部状态的时候,例如,我们想要让Account对象之间可以比较它们的余额的差额但又希望把各自的余额隐藏起来(也许是因为我们把它们放在不同的表格中)。

class Account
attr_reader :balance # 访问方法 'balance'
protected :balance # 把它私有化
def greaterBalanceThan(other)
return @balance > other.balance
end
end

因为balance方法是保护的,所以它只能被Account类的对象访问。


变量[ ]

现在我们创建了所有这些对象,但是问题是要保证不会丢失它们,变量就是用来跟踪对象的,每个变量保存一个对象的引用,看看下面的代码。

person = "Tim"
person.id >> 537771100
person.type >> String
person >> "Tim"


第一行,我们使用"Tim"创建了一个新的String对象,对这个对象的一个引用被放置在本地变量person中,上面的简单测试显示变量和对象的id、类型、值确实保存在一个字符串名字当中。

那么,变量是对象吗?

答案是NO!一个变量只是一个对象的简单引用,对象漂浮在某处的大池子里(堆栈,大多数时候是这里),变量指向对象。

我们再看一个稍微复杂的例子

person1 = "Tim" v person2 = person1
person1[0] = 'J'
person1 >> "Jim"
person2 >> "Jim"


怎么回事?我们改变了person1的第一个字符,但是person1和person2都从"Tim"变成了"Jim"。

这是因为变量保存对象的引用,而不是对象本身。把person1赋值给person2并不创建新的对象,只是简单地把person1的对象引用拷贝给person2,所以person1和person2都指向同一对象。

赋值给了对象一个别名,可能同一个对象会有多个变量引用,这会不会给你的代码带来问题呢?会,不过不想你想象的那么麻烦(Jaca的对象也是这样的)。上面的例子中,你可以使用String的dup方法来避免混淆,它创建一个有着相同内容的新String对象。

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 >>[/tab]"Jim"
person2 >>[/tab]"Tim"


如果你不想别人修改一个特定的对象,可以冻结它(我们在251页详细讨论冻结对象)。尝试修改一个被冻结的对象,Ruby会抛出一个TypeError异常。

person1 = "Tim"
person2 = person1
person1.freeze # 避免对象被修改
person2[0] = "J"
produces:
prog.rb:4:in '=': can't modify frozen string (TypeError)
from prog.rb:4