?
This document uses PHP Chinese website manual Release
??? 目錄和播放列表都有類似的操作,增加歌曲,刪除歌曲,返回歌曲列表等等。播放列表可能還需要別的方法,比如插入廣告,記錄累計播放時間等,我們將在后面考率這些問題?,F(xiàn)在,我們需要建立一個SongList類,以便在目錄和播放列表中使用。
??? 在開始實現(xiàn)之前,我們要知道怎么在SongList中存儲歌曲列表。我們有三個明顯得選擇:Ruby的提供的Array,Ruby提供的Hash,或者我們自己創(chuàng)建一個列表結(jié)構(gòu)。下面我們來看看數(shù)組合和哈希,然后選擇一個來在我們的類中使用。
數(shù)組類包含了若干個對其它對象的引用,每個引用在數(shù)組的一個位置上,用一個正整數(shù)來索引。
你可以直接指定一組值來創(chuàng)建一個數(shù)組,或者用數(shù)組類的new方法。用第一種方法創(chuàng)建數(shù)組要用中括號把各個元素括起來,每個元素之間用逗號隔開。
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"] |
數(shù)組指定索引的下標(biāo)用[ ]操作符,這實際上也是一個方法,可以在子類中被重載。數(shù)組索引從0開始,從左向右技術(shù),如果指定的索引為負(fù)數(shù),則表示從右邊開始計數(shù)(這時候最右邊的索引下標(biāo)為-1,不是0)。
a?=?[?1,?3,?5,?7,?9?] | ||
a[-1] |
? |
9 |
a[-2] |
? |
7 |
a[-99] |
? |
nil |
你也可以一對數(shù)字[start,?count]
作為下標(biāo),這將返回一個數(shù)組,包含原數(shù)組中從start開始的count個元素。
a?=?[?1,?3,?5,?7,?9?] | ||
a[1,?3] |
? |
[3,?5,?7] |
a[3,?1] |
? |
[7] |
a[-3,?2] |
? |
[5,?7] |
??? 最后,你也可以在數(shù)組下標(biāo)中使用Range作為索引。Range可參看下一章。Range的起始值之間如果是2個點號,則包括最后的結(jié)束值,如果是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] |
[ ]方法有一個對應(yīng)的 []=
方法,這個方法是用來給指定的位置設(shè)置新的值,如果數(shù)組的下標(biāo)位置超過了數(shù)組的大小,那么指定的下標(biāo)將被設(shè)為指定的值,而中間沒有涉及到的位置為設(shè)為nil。如下面例子數(shù)組最大下標(biāo)為4,而對a[6]設(shè)置新值,a[5]則被設(shè)為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] |
如果在[]=
方法中下標(biāo)索引為兩個數(shù)字(一個開始位置,一個長度)或者一個Range,則對應(yīng)的一系列值將被重新設(shè)置。如果第二個參數(shù)即長度為0,則右邊的值將被插入當(dāng)前位置,沒有值會被刪除。如果[ ]=右邊的長度和左邊下標(biāo)中指定的長度不一樣,則原數(shù)組自動進(jìn)行調(diào)整。
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] |
數(shù)組有很多有用的方法,使用這些方法你能用數(shù)組實現(xiàn)隊列,堆棧,列表等各種數(shù)據(jù)結(jié)構(gòu)。
?
=>
?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中修改[ ] 方法,使它能接受一個字符串參數(shù),返回以此為標(biāo)題的歌曲的??雌饋砦覀兒苋菀卓梢詫崿F(xiàn):我們有一個包含了很多Song對象的對象的數(shù)組,我們只需循環(huán)遍歷整個數(shù)組,找到匹配的那個就可以了。
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 |
這樣已經(jīng)可以工作了,而且看上去很符合常規(guī):用一個for循環(huán)來遍歷數(shù)組。
有沒有更自然的方法呢?
當(dāng)然有,我們可以用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就是一個迭代器,這個方法重復(fù)不斷地執(zhí)行一個給定的block。塊和迭代器都是Ruby中比較有趣的特點。我們后面會進(jìn)一步來討論這些特點。
一個Ruby迭代器就是一個簡單的能接收代碼塊的方法。第一眼看上去,Ruby中的block像C,Java,Perl中的一樣,但是實際上是有不同的。
首先,塊在源代碼中緊挨著方法調(diào)用,并且和這個方法的最后一個參數(shù)寫在同一行上。其次,這個塊不會立即被執(zhí)行,Ruby首先會記住這個塊出現(xiàn)的上下文(局部變量,當(dāng)前對象等),然后進(jìn)入方法,這里也是魔術(shù)開始的地方。
在方法里面,這個塊才會用yield來調(diào)用執(zhí)行,就像這個塊是方法本身一樣,每當(dāng)yield在方法中被執(zhí)行,這個塊就會被調(diào)用。當(dāng)這個塊執(zhí)行完退出后,控制將交給yield后面的語句(yield來自一個有20多年歷史的語言:CLU)。我們來看一個小例子。
def?threeTimes ??yield ??yield ??yield end threeTimes?{?puts?"Hello"?} |
Hello Hello Hello |
這個塊(用兩個大括號定義)賦給了一個方法threeTimes,在這個方法里面,yield執(zhí)行了3次,每次執(zhí)行它都會調(diào)用給定的block,即打印一個歡迎語句。使塊變得有趣的是你可以給塊傳遞參數(shù),并且從塊中得到結(jié)果。下面例子,我們將會得到小于一個指定值得Fibonacci 數(shù)列。
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接收一個參數(shù),這個參數(shù)將會在執(zhí)行的時候傳遞給指定的塊。在塊的定義中,參數(shù)用兩個豎線括起來,放在最前面。在這個例子中f用來接收yield傳遞的參數(shù),所以,這個塊才能打印這個序列。一個塊可以接受任意個參數(shù)。如果一個塊的參數(shù)和yield中傳遞的參數(shù)個數(shù)不一樣,將會怎樣呢?很巧合,這和我們在并行賦值(parallel assignment)中談到的原則一樣(如果一個block只接收一個參數(shù),而yield提供的參數(shù)多于1個,那么這些參數(shù)將被轉(zhuǎn)化為一個數(shù)組。)
傳遞給一個塊的參數(shù)可以是存在地局部變量,如果是這樣的話,那么這個局部變量的新值(如果在塊中被修改了)在塊退出后將會保留,這可能會有一定的副作用,但是這樣做有一個性能方面的考率。一個塊也可以返回一個結(jié)果給調(diào)用它的方法。這個塊中的最后一個表達(dá)式的值將會返回給方法,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 |
這個用法中數(shù)組將連續(xù)的元素傳遞給指定的塊,如果這個塊返回true,則這個方法返回當(dāng)前對應(yīng)的元素值,如果沒有符合的值,則返回nil。這個方法顯示了迭代器的好處,Array類只作自己應(yīng)該做的,訪問數(shù)組元素,而應(yīng)用代碼只關(guān)注于特殊的需求。
Ruby中的集合對象中也包含其它一些常用迭代器,其中之二是each和collect。each可以認(rèn)為是最簡單的迭代器,它們都會對集合的每個元素來調(diào)用塊。
[?1,?3,?5?].each?{?|i|?puts?i?} |
1 3 5 |
另一個是collect,它跟each類似,它將集合中的元素傳遞給一個塊,在塊中處理后返回一個包含處理結(jié)果的新數(shù)組。
["H",?"A",?"L"].collect?{?|x|?x.succ?} |
? |
["I",?"B",?"M"] |
這值得我們再花一些時間來比較一下Ruby,C++,JAVA中的迭代器。在Ruby中,迭代器是一個簡單的方法,每當(dāng)它產(chǎn)生一個新值,都會調(diào)用yield方法。使用迭代器只需要給這個迭代器傳遞一個塊,不需要像C++,Java中那樣創(chuàng)建輔助類來處理迭代器的狀態(tài)。從這一點和其它一些特點來說,Ruby是一種透明語言,當(dāng)你寫程序的時候,你只需關(guān)注于讓功能能夠?qū)崿F(xiàn),而不必編寫腳手架來支持語言的一些功能。
迭代器不僅僅用在數(shù)組和哈希等集合結(jié)構(gòu)上,它也能返回上面Fibonacci 例子中那樣的序列值,Ruby中的輸入輸出類也用到了迭代器,這些類實現(xiàn)了迭代器接口,每次返回一個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... |
讓我們再看看另外一個迭代器實現(xiàn)。Smalltalk也支持迭代器,如果你用smalltalk語言來計算一個數(shù)組中元素的和,可以這樣:
sumOfValues??????????????"Smalltalk?method" ????^self?values ??????????inject:?0 ??????????into:?[?:sum?:element?|?sum?+?element?value] |
inject
這樣工作:第一次指定的block被執(zhí)行的時候,sum被設(shè)成inject的參數(shù)(本例為0),element被設(shè)為數(shù)組的第一個元素。以后block被執(zhí)行的時候,sum的值設(shè)為上次block執(zhí)行后返回的值。這樣,sum就可以記錄總數(shù)了最后的inject的值是block最后一次執(zhí)行后返回的值。
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經(jīng)常是用在迭代器中,但是,塊也有其它一些有用的用處。
block也可以用作定義一塊代碼,這些代碼必須在一定的事務(wù)控制下運行。比如,你經(jīng)常會打開一個文件,對內(nèi)容作一些處理,然后需要確保文件在最后會被關(guān)閉。盡管我們可以用常規(guī)的方法實現(xiàn),但是,我們可以讓文件對象自己負(fù)責(zé)關(guān)閉它。一個簡單例子如下(忽略了錯誤處理等):
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... |
這個小例子闡述了幾個技術(shù)點。方法openAndProcess
是一個類方法,這個方法可以獨立于任何File對象來單獨調(diào)用,即不需要生成類的實例。我們在這個方法的參數(shù)列表中使用了 *args
,這表示所有調(diào)用時候的參數(shù)將作為數(shù)組傳遞到這個方法。而這個參數(shù)是傳遞給File.open方法的。
一旦這個文件被打開,openAndProcess
將調(diào)用yield,然后將打開的文件對象傳遞給這個block。當(dāng)block返回后,這個文件將被關(guān)閉。這種情況下,關(guān)閉文件的任務(wù)就從使用文件對象的用戶轉(zhuǎn)變?yōu)槲募旧砹恕?/p>
最后,這個例子用do..end來定義一個塊,這和用兩個打括號定義一個塊只是有優(yōu)先級別的區(qū)別,將在后面討論。
讓文件自己管理自己的生命周期十分有用,Ruby自帶的File類就提供了這樣的支持。如果File.open
調(diào)用時指定了一個block,那么這個塊就會用傳遞過來文件對象作為參數(shù)執(zhí)行,然后,當(dāng)塊結(jié)束后,這個文件會被文件對象關(guān)閉。這很有趣,也就是說File.open
方法有兩個版本,一個接受block,一個不接受block。當(dāng)沒有指定block調(diào)用這個方法的時候,這個方法只會返回一個打開的文件對象。方法Kernel::block_given?
提供了實現(xiàn)這種功能的可能,如果調(diào)用的時候給了一個block,這個方法將返回true,這樣,你也可以實現(xiàn)自己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 |
讓我們再回來看看自動點唱機(jī),某些時候,我們需要處理點唱機(jī)和用戶的界面:很多按鈕,供用戶選擇歌曲和控制播放,我們需要給這些按鈕指定相應(yīng)的時間處理代碼,而Ruby中的block就很適合干這樣的事情。假設(shè)點唱機(jī)的設(shè)計者通過Ruby擴(kuò)展為我們提供了一個基本的按鈕類。
bStart?=?Button.new("Start") bPause?=?Button.new("Pause") #?... |
當(dāng)用戶按下按鈕之后如何處理呢?Button類提供了一個buttonPressed
方法,在按鈕按下時能被回調(diào)。所以最簡單的方法創(chuàng)建Button的子類,然后在每個子類中實現(xiàn)buttonPressed
方法。
class?StartButton?<?Button ??def?initialize ????super("Start")???????#?invoke?Button's?initialize ??end ??def?buttonPressed ????#?do?start?actions... ??end endbStart?=?StartButton.new |
這樣做有兩個問題,首先這樣將產(chǎn)生大量子類,如果Button變化了,所有子類都需要維護(hù);第二,按鈕按下之后需要執(zhí)行的行為不應(yīng)該在按鈕上表示,這是點唱機(jī)的責(zé)任。我們可以用塊來消除這些問題
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?} |
上面代碼的關(guān)鍵點在JukeboxButton#initialize
的第二個參數(shù),這個參數(shù)前面帶有一個&符號,在Ruby中代表這個參數(shù)是一個塊,這個塊將會被轉(zhuǎn)化為一個Proc對象,并關(guān)聯(lián)道相應(yīng)變量。在本例中我們把它賦給實例變量@action 。
當(dāng)回調(diào)方法buttonPressed
執(zhí)行時,我們用方法Proc#call
來調(diào)用相應(yīng)的block。
我們來看一個例子,這個例子用了方法proc,這個方法把一個block變?yōu)榱艘粋€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對象,這個對象引用了這個函數(shù)的參數(shù):aThing
。
即使這個參數(shù)在塊被調(diào)用時已經(jīng)不在自己的作用域里了,這個塊還是可以訪問這個參數(shù)。(比如p1.call(4),
這個時候,雖然對p1的賦值語句中的參數(shù)23已經(jīng)超出了作用域范圍,但是它仍然保存著23×4=92。)