©
本文檔使用 php中文網手册 發布
目录和播放列表都有类似的操作,增加歌曲,删除歌曲,返回歌曲列表等等。播放列表可能还需要别的方法,比如插入广告,记录累计播放时间等,我们将在后面考率这些问题。现在,我们需要建立一个SongList类,以便在目录和播放列表中使用。
在开始实现之前,我们要知道怎么在SongList中存储歌曲列表。我们有三个明显得选择:Ruby的提供的Array,Ruby提供的Hash,或者我们自己创建一个列表结构。下面我们来看看数组合和哈希,然后选择一个来在我们的类中使用。
数组类包含了若干个对其它对象的引用,每个引用在数组的一个位置上,用一个正整数来索引。
你可以直接指定一组值来创建一个数组,或者用数组类的new方法。用第一种方法创建数组要用中括号把各个元素括起来,每个元素之间用逗号隔开。
a = [ 3.14159, "pie", 99 ] | ||
a.type |
? |
Array |
a.length |
? |
3 |
a[0] |
? |
3.14159 |
a[1] |
? |
"pie" |
a[2] |
? |
99 |
a[3] |
? |
nil |
| ||
b = Array.new | ||
b.type |
? |
Array |
b.length |
? |
0 |
b[0] = "second" | ||
b[1] = "array" | ||
b |
? |
["second", "array"] |
数组指定索引的下标用[ ]操作符,这实际上也是一个方法,可以在子类中被重载。数组索引从0开始,从左向右技术,如果指定的索引为负数,则表示从右边开始计数(这时候最右边的索引下标为-1,不是0)。
a = [ 1, 3, 5, 7, 9 ] | ||
a[-1] |
? |
9 |
a[-2] |
? |
7 |
a[-99] |
? |
nil |
你也可以一对数字[start, count]
作为下标,这将返回一个数组,包含原数组中从start开始的count个元素。
a = [ 1, 3, 5, 7, 9 ] | ||
a[1, 3] |
? |
[3, 5, 7] |
a[3, 1] |
? |
[7] |
a[-3, 2] |
? |
[5, 7] |
最后,你也可以在数组下标中使用Range作为索引。Range可参看下一章。Range的起始值之间如果是2个点号,则包括最后的结束值,如果是3个点号,则Range的值不包括最后的那个边界值。
a = [ 1, 3, 5, 7, 9 ] | ||
a[1..3] |
? |
[3, 5, 7] |
a[1...3] |
? |
[3, 5] |
a[3..3] |
? |
[7] |
a[-3..-1] |
? |
[5, 7, 9] |
[ ]方法有一个对应的 []=
方法,这个方法是用来给指定的位置设置新的值,如果数组的下标位置超过了数组的大小,那么指定的下标将被设为指定的值,而中间没有涉及到的位置为设为nil。如下面例子数组最大下标为4,而对a[6]设置新值,a[5]则被设为nil。
a = [ 1, 3, 5, 7, 9 ] | ? | [1, 3, 5, 7, 9] |
a[1] = 'bat' | ? | [1, "bat", 5, 7, 9] |
a[-3] = 'cat' | ? | [1, "bat", "cat", 7, 9] |
a[3] = [ 9, 8 ] | ? | [1, "bat", "cat", [9, 8], 9] |
a[6] = 99 | ? | [1, "bat", "cat", [9, 8], 9, nil, 99] |
如果在[]=
方法中下标索引为两个数字(一个开始位置,一个长度)或者一个Range,则对应的一系列值将被重新设置。如果第二个参数即长度为0,则右边的值将被插入当前位置,没有值会被删除。如果[ ]=右边的长度和左边下标中指定的长度不一样,则原数组自动进行调整。
a = [ 1, 3, 5, 7, 9 ] | ? | [1, 3, 5, 7, 9] |
a[2, 2] = 'cat' | ? | [1, 3, "cat", 9] |
a[2, 0] = 'dog' | ? | [1, 3, "dog", "cat", 9] |
a[1, 1] = [ 9, 8, 7 ] | ? | [1, 9, 8, 7, "dog", "cat", 9] |
a[0..3] = [] | ? | ["dog", "cat", 9] |
a[5] = 99 | ? | ["dog", "cat", 9, nil, nil, 99] |
数组有很多有用的方法,使用这些方法你能用数组实现队列,堆栈,列表等各种数据结构。
=>
value pairs between braces.
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' } | ||
| ||
h.length |
? |
3 |
h['dog'] |
? |
"canine" |
h['cow'] = 'bovine' | ||
h[12] = 'dodecine' | ||
h['cat'] = 99 | ||
h |
? |
{"cow"=>"bovine", "cat"=>99, 12=>"dodecine", "donkey"=>"asinine", "dog"=>"canine"} |
Hash
starts on page 317.
SongList
. Let's invent a basic list of methods we need in our SongList
. We'll want to add to it as we go along, but it will do for now.
Array
. Similarly, the ability to return a song at an integer position in the list is supported by arrays.
However, there's also the need to be able to retrieve songs by title, which might suggest using a hash, with the title as a key and the song as a value. Could we use a hash? Well, possibly, but there are problems. First a hash is unordered, so we'd probably need to use an ancillary array to keep track of the list. A bigger problem is that a hash does not support multiple keys with the same value. That would be a problem for our playlist, where the same song might be queued up for playing multiple times. So, for now we'll stick with an array of songs, searching it for titles when needed. If this becomes a performance bottleneck, we can always add some kind of hash-based lookup later.
We'll start our class with a basic initialize
method, which creates the Array
we'll use to hold the songs and stores a reference to it in the instance variable @songs
.
class SongList def initialize @songs = Array.new end end |
SongList#append
method adds the given song to the end of the @songs
array. It also returns self, a reference to the current SongList
object. This is a useful convention, as it lets us chain together multiple calls to append
. We'll see an example of this later.
class SongList def append(aSong) @songs.push(aSong) self end end |
deleteFirst
and deleteLast
methods, trivially implemented using
Array#shift
and
Array#pop
, respectively.
class SongList def deleteFirst @songs.shift end def deleteLast @songs.pop end end |
append
returns the SongList
object to chain together these method calls.
list = SongList.new list. append(Song.new('title1', 'artist1', 1)). append(Song.new('title2', 'artist2', 2)). append(Song.new('title3', 'artist3', 3)). append(Song.new('title4', 'artist4', 4)) |
nil
is returned when the list becomes empty.
list.deleteFirst |
? |
Song: title1--artist1 (1) |
list.deleteFirst |
? |
Song: title2--artist2 (2) |
list.deleteLast |
? |
Song: title4--artist4 (4) |
list.deleteLast |
? |
Song: title3--artist3 (3) |
list.deleteLast |
? |
nil |
[]
, which accesses elements by index. If the index is a number (which we check using
Object#kind_of?
), we just return the element at that position.
class SongList def [](key) if key.kind_of?(Integer) @songs[key] else # ... end end end |
list[0] |
? |
Song: title1--artist1 (1) |
list[2] |
? |
Song: title3--artist3 (3) |
list[9] |
? |
nil |
下面,我们要在SongList中修改[ ] 方法,使它能接受一个字符串参数,返回以此为标题的歌曲的。看起来我们很容易可以实现:我们有一个包含了很多Song对象的对象的数组,我们只需循环遍历整个数组,找到匹配的那个就可以了。
class SongList def [](key) if key.kind_of?(Integer) return @songs[key] else for i in 0...@songs.length return @songs[i] if key == @songs[i].name end end return nil end end |
这样已经可以工作了,而且看上去很符合常规:用一个for循环来遍历数组。
有没有更自然的方法呢?
当然有,我们可以用Array的find方法。
class SongList def [](key) if key.kind_of?(Integer) result = @songs[key] else result = @songs.find { |aSong| key == aSong.name } end return result end end |
我们可以用if修饰符来使代码更简短一些。
class SongList def [](key) return @songs[key] if key.kind_of?(Integer) return @songs.find { |aSong| aSong.name == key } end end |
方法find就是一个迭代器,这个方法重复不断地执行一个给定的block。块和迭代器都是Ruby中比较有趣的特点。我们后面会进一步来讨论这些特点。
一个Ruby迭代器就是一个简单的能接收代码块的方法。第一眼看上去,Ruby中的block像C,Java,Perl中的一样,但是实际上是有不同的。
首先,块在源代码中紧挨着方法调用,并且和这个方法的最后一个参数写在同一行上。其次,这个块不会立即被执行,Ruby首先会记住这个块出现的上下文(局部变量,当前对象等),然后进入方法,这里也是魔术开始的地方。
在方法里面,这个块才会用yield来调用执行,就像这个块是方法本身一样,每当yield在方法中被执行,这个块就会被调用。当这个块执行完退出后,控制将交给yield后面的语句(yield来自一个有20多年历史的语言:CLU)。我们来看一个小例子。
def threeTimes yield yield yield end threeTimes { puts "Hello" } |
Hello Hello Hello |
这个块(用两个大括号定义)赋给了一个方法threeTimes,在这个方法里面,yield执行了3次,每次执行它都会调用给定的block,即打印一个欢迎语句。使块变得有趣的是你可以给块传递参数,并且从块中得到结果。下面例子,我们将会得到小于一个指定值得Fibonacci 数列。
def fibUpTo(max) i1, i2 = 1, 1 # parallel assignment while i1 <= max yield i1 i1, i2 = i2, i1+i2 end end fibUpTo(1000) { |f| print f, " " } |
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 |
在这个例子中,yield接收一个参数,这个参数将会在执行的时候传递给指定的块。在块的定义中,参数用两个竖线括起来,放在最前面。在这个例子中f用来接收yield传递的参数,所以,这个块才能打印这个序列。一个块可以接受任意个参数。如果一个块的参数和yield中传递的参数个数不一样,将会怎样呢?很巧合,这和我们在并行赋值(parallel assignment)中谈到的原则一样(如果一个block只接收一个参数,而yield提供的参数多于1个,那么这些参数将被转化为一个数组。)
传递给一个块的参数可以是存在地局部变量,如果是这样的话,那么这个局部变量的新值(如果在块中被修改了)在块退出后将会保留,这可能会有一定的副作用,但是这样做有一个性能方面的考率。一个块也可以返回一个结果给调用它的方法。这个块中的最后一个表达式的值将会返回给方法,Array中的find方法就是这样工作的。(find在Enumerable
中定义,被插入到了类Array
)
class Array | ||
def find | ||
for i in 0...size | ||
value = self[i] | ||
return value if yield(value) | ||
end | ||
return nil | ||
end | ||
end | ||
| ||
[1, 3, 5, 7, 9].find {|v| v*v > 30 } |
? |
7 |
这个用法中数组将连续的元素传递给指定的块,如果这个块返回true,则这个方法返回当前对应的元素值,如果没有符合的值,则返回nil。这个方法显示了迭代器的好处,Array类只作自己应该做的,访问数组元素,而应用代码只关注于特殊的需求。
Ruby中的集合对象中也包含其它一些常用迭代器,其中之二是each和collect。each可以认为是最简单的迭代器,它们都会对集合的每个元素来调用块。
[ 1, 3, 5 ].each { |i| puts i } |
1 3 5 |
另一个是collect,它跟each类似,它将集合中的元素传递给一个块,在块中处理后返回一个包含处理结果的新数组。
["H", "A", "L"].collect { |x| x.succ } |
? |
["I", "B", "M"] |
这值得我们再花一些时间来比较一下Ruby,C++,JAVA中的迭代器。在Ruby中,迭代器是一个简单的方法,每当它产生一个新值,都会调用yield方法。使用迭代器只需要给这个迭代器传递一个块,不需要像C++,Java中那样创建辅助类来处理迭代器的状态。从这一点和其它一些特点来说,Ruby是一种透明语言,当你写程序的时候,你只需关注于让功能能够实现,而不必编写脚手架来支持语言的一些功能。
迭代器不仅仅用在数组和哈希等集合结构上,它也能返回上面Fibonacci 例子中那样的序列值,Ruby中的输入输出类也用到了迭代器,这些类实现了迭代器接口,每次返回一个I/O流的下一行。f = File.open("testfile") f.each do |line| print line end f.close |
This is line one This is line two This is line three And so on... |
让我们再看看另外一个迭代器实现。Smalltalk也支持迭代器,如果你用smalltalk语言来计算一个数组中元素的和,可以这样:
sumOfValues "Smalltalk method" ^self values inject: 0 into: [ :sum :element | sum + element value] |
inject
这样工作:第一次指定的block被执行的时候,sum被设成inject的参数(本例为0),element被设为数组的第一个元素。以后block被执行的时候,sum的值设为上次block执行后返回的值。这样,sum就可以记录总数了最后的inject的值是block最后一次执行后返回的值。
Ruby 没有inject
方法,但我们可以很容易的写一个,在这个例子中我们把它加入Array类。
class Array | ||
def inject(n) | ||
each { |value| n = yield(n, value) } | ||
n | ||
end | ||
def sum | ||
inject(0) { |n, value| n + value } | ||
end | ||
def product | ||
inject(1) { |n, value| n * value } | ||
end | ||
end | ||
[ 1, 2, 3, 4, 5 ].sum |
? |
15 |
[ 1, 2, 3, 4, 5 ].product |
? |
120 |
尽管block经常是用在迭代器中,但是,块也有其它一些有用的用处。
block也可以用作定义一块代码,这些代码必须在一定的事务控制下运行。比如,你经常会打开一个文件,对内容作一些处理,然后需要确保文件在最后会被关闭。尽管我们可以用常规的方法实现,但是,我们可以让文件对象自己负责关闭它。一个简单例子如下(忽略了错误处理等):
class File def File.openAndProcess(*args) f = File.open(*args) yield f f.close() end end File.openAndProcess("testfile", "r") do |aFile| print while aFile.gets end |
This is line one This is line two This is line three And so on... |
这个小例子阐述了几个技术点。方法openAndProcess
是一个类方法,这个方法可以独立于任何File对象来单独调用,即不需要生成类的实例。我们在这个方法的参数列表中使用了 *args
,这表示所有调用时候的参数将作为数组传递到这个方法。而这个参数是传递给File.open方法的。
一旦这个文件被打开,openAndProcess
将调用yield,然后将打开的文件对象传递给这个block。当block返回后,这个文件将被关闭。这种情况下,关闭文件的任务就从使用文件对象的用户转变为文件本身了。
最后,这个例子用do..end来定义一个块,这和用两个打括号定义一个块只是有优先级别的区别,将在后面讨论。
让文件自己管理自己的生命周期十分有用,Ruby自带的File类就提供了这样的支持。如果File.open
调用时指定了一个block,那么这个块就会用传递过来文件对象作为参数执行,然后,当块结束后,这个文件会被文件对象关闭。这很有趣,也就是说File.open
方法有两个版本,一个接受block,一个不接受block。当没有指定block调用这个方法的时候,这个方法只会返回一个打开的文件对象。方法Kernel::block_given?
提供了实现这种功能的可能,如果调用的时候给了一个block,这个方法将返回true,这样,你也可以实现自己open方法:
class File def File.myOpen(*args) aFile = File.new(*args) # If there's a block, pass in the file and close # the file when it returns if block_given? yield aFile aFile.close aFile = nil end return aFile end end |
让我们再回来看看自动点唱机,某些时候,我们需要处理点唱机和用户的界面:很多按钮,供用户选择歌曲和控制播放,我们需要给这些按钮指定相应的时间处理代码,而Ruby中的block就很适合干这样的事情。假设点唱机的设计者通过Ruby扩展为我们提供了一个基本的按钮类。
bStart = Button.new("Start") bPause = Button.new("Pause") # ... |
当用户按下按钮之后如何处理呢?Button类提供了一个buttonPressed
方法,在按钮按下时能被回调。所以最简单的方法创建Button的子类,然后在每个子类中实现buttonPressed
方法。
class StartButton < Button def initialize super("Start") # invoke Button's initialize end def buttonPressed # do start actions... end endbStart = StartButton.new |
这样做有两个问题,首先这样将产生大量子类,如果Button变化了,所有子类都需要维护;第二,按钮按下之后需要执行的行为不应该在按钮上表示,这是点唱机的责任。我们可以用块来消除这些问题
class JukeboxButton < Button def initialize(label, &action) super(label) @action = action end def buttonPressed @action.call(self) end end bStart = JukeboxButton.new("Start") { songList.start } bPause = JukeboxButton.new("Pause") { songList.pause } |
上面代码的关键点在JukeboxButton#initialize
的第二个参数,这个参数前面带有一个&符号,在Ruby中代表这个参数是一个块,这个块将会被转化为一个Proc对象,并关联道相应变量。在本例中我们把它赋给实例变量@action 。
当回调方法buttonPressed
执行时,我们用方法Proc#call
来调用相应的block。
我们来看一个例子,这个例子用了方法proc,这个方法把一个block变为了一个Proc对象。
def nTimes(aThing) | ||
return proc { |n| aThing * n } | ||
end | ||
| ||
p1 = nTimes(23) | ||
p1.call(3) |
? |
69 |
p1.call(4) |
? |
92 |
p2 = nTimes("Hello ") | ||
p2.call(3) |
? |
"Hello Hello Hello " |
方法nTimes
返回一个Proc对象,这个对象引用了这个函数的参数:aThing
。
即使这个参数在块被调用时已经不在自己的作用域里了,这个块还是可以访问这个参数。(比如p1.call(4),
这个时候,虽然对p1的赋值语句中的参数23已经超出了作用域范围,但是它仍然保存着23×4=92。)