迭代器 (iterator) 並不是 Ruby 的原創概念, 這已在物件導向程式語言中普遍使用。它也被用在 Lisp 中,只是並不稱為迭代器而已。然而很多人仍然不熟悉迭代器的概念,因此以下將詳細解釋。
迭代 (iterate) 指的是重複做相同的事,所以迭代器 (iterator) 就是用來重複多次相同的事。
我們編寫程式碼後,需要在不同的情況下迴圈。使用 C 時,我會用 for
或 while
。例如,
for (str = "abcdefg"; *str != '\0'; str++) {
/* process a character here */
}
C 的 for(...)
語法提供建立迴圈的抽象概念,但測試空字元 (null character) 的 *str
程式碼,需要程式員了解字串的內部結構。如此讓人覺得 C 是低階的語言。高階語言應更能彈性地支援迭代。請看一下的 sh
shell 腳本:
for i in *.[ch]; do
# ... 針對每個檔案執行某件事
done
處理目前目錄下的所有 C source 及標頭檔,由命令 shell 負責提取檔案名稱,並一一替代。這樣比 C 看來高階得多吧,你說呢?
但我們還要留意:雖然語言能夠為內建資料型態提供迭代器,但如果我們得為自定的資料型態編寫低階的迴圈才能迭代,就太令人失望了。畢竟使用 OOP(物件導向程式)時,我們經常要一一定義資料型態,這就成了嚴重的問題。
因此每種 OOP 語言都有些方式有助於迭代。有些語言會提供特殊的類別;Ruby 則讓我們直接定義迭代。
Ruby 字串 (string)
型態具有一些實用的迭代器:
<a><b><c>
nil
each_byte
是字串中用於處理每個字元的迭代器。每個字元都會代入區域變數 c
中。這段程式也可以改寫為類似 C 語言的寫法...
0
ruby> while i<s.length
| printf "<%c>", s[i]; i+=1
| end; print "\n"
<a><b><c>
nil
⋯⋯然而,each_byte
迭代器的概念較簡單,而且即使之後字串 (string)
類別的結構大幅改變,仍有較大機會繼續運作。迭代器的好處在於面對變化仍能保持耐用,確實稱得上是良好的程式碼。(好的,耐心點,我們很快就會討論類別 (class))
字串 (string)
的另一個迭代器是 each_line
。
a
b
c
nil
比起 C 程式,迭代器更能事半功倍(尋找行分隔符號 (line delimiter)、建立子字串等)。
前一節的 for
敘述就是使用 each
迭代器進行迭代的。字串 (string)
的 each
與 each_line
效果相同(編註:Ruby 1.9 之後版本,移除了字串的 each
函式,所以必須改用 each_line
。此例在 1.9 之後版本無法執行。),那麼就將上述的範例用 for
改寫:
| print l
| end
a
b
c
nil
將控制結構 retry
與迭代的迴圈一起運作,就能重新開始迴圈。
0
ruby> for i in 0..4
| print i
| if i == 2 and c == 0
| c = 1
| print "\n"
| retry
| end
| end; print "\n"
012
01234
nil
將 retry
替換為 redo
,會令目前的迴圈重做一次迭代,輸出內容為:
234
yield
有時會出現在迭代器的定義中。yield
可將控制移至傳遞往迭代器的程式碼區塊 (block of code)(這將在程序物件一節中討論)。以下範例定義了迭代器 repeat
,這個迭代器可按引數所指定的次數,重複程式碼區塊。
| while num > 0
| yield
| num -= 1
| end
| end
nil
ruby> repeat(3) { puts "foo" }
foo
foo
foo
nil
使用 retry
,就能定義類似 Ruby 標準的 while
迭代器。
| return if not cond
| yield
| retry
| end
nil
ruby> i=0; WHILE(i<3) { print i; i+=1 }
012 nil
你明白甚麼是迭代器了嗎?雖然仍有些限制,但你已經可以自行編寫迭代器。事實上,每當定義新資料型態時,常會一併定義合適的迭代器,方便作為搭配。因此,上述範例並不說得上非常有用。了解甚麼是類別 (class) 後,再來討論一些實用的迭代器吧。