第七課 迭代器與泛型for

迭代器就是一種可以遍歷一種集合中所有元素的機制。
在Lua中,通常將迭代器表示爲函數。每調用一次函數,即返回集合中的“下一個”元素。
每個迭代器都需要在每次成功調用之間保持一些狀態,這樣才能知道它所在的位置及如何步進到下個位置。closure對於這類任務提供了極佳的支持,一個closure就是一種可以訪問其外部嵌套環境中的局部變量的函數。
爲了創建一個新的closure,還必須要創建它的這些“非局部的變量”。因此一個closure結構通常涉及到兩個函數:closure本身以及一個用於創建該closure的工廠函數。如爲一個列表 編寫一個簡單的迭代器。與ipairs不同的是該迭代器並不是返回每個元素的索引,而是返回元素的值:
function values(t)
local i = 0
return function() i = i + 1; return t[i] end
end
values就是一個工廠。
t = {10, 20, 30}
iter = values(t) --創建迭代器
while true do
local element = iter() --調用迭代器
if element == nil then break end
print(element)
end
使用泛型for則更爲簡單。接下來會發現,它正是爲這種迭代而設計的:
t = {10, 20, 30}
for element in values(t) do
print(element)
end
泛型for爲每次迭代循環做了所有的 簿記工作。它在內部保存了迭代器函數,因此不再需要iter變量。它在每次新迭代時調用迭代器,並且在迭代器返回nil時結束循環。
高級的迭代器示例:一個可以遍歷當前輸入文件中所有單詞的迭代器---allwords。(一行一行的處理)
function allwords ()
local line = io.read() --當前行
local pos = 1 --一行中的當前位置
return function ()
while line do
local s, e = string.find(line, "%w+", pos) --使用模式匹-----配%w+匹配一個或多個的文字/數字字符。
if s then --是否找到一個單詞
pos = e + 1 --該單詞的下個位置
return string.sub(line, s, e) --返回該單詞
else
line = io.read() --沒找到單詞,繼續下一行
pos = 1 --在第一個位置重新開始
end
end
return nil --沒有新行,結束遍歷
end
end
調用allwords迭代器很簡單:
for word in allwords() do
print(word)
end

泛型for的語義
上述的那些迭代器都有一個缺點,就是需要爲每個新的循環創建一個新的closure。某些情況下,這些開銷就不太容易接受了。因此,在這類情況中,希望通過泛型for的自身來保存迭代器狀態。
泛型for在循環過程內部保存了迭代器函數。實際上它保存了3個值:一個迭代器函數、一個恆定狀態和一個控制變量。
泛型for的語法如下:
for <var-list> in <exp-list> do
<body>
end
其中,<var-list>是一個或多個變量名的列表,以逗號分割;<exp-list>是一個或多個表達式的列表,同樣以逗號分割。通常表達式列表只有一個元素,即一句對迭代器工廠的調用。如:
for k, v in pairs(t) do print(k, v) end
其中,變量列表是“k, v”,表達式列表只有一個元素pairs(t)。一般來說變量列表中也只有一個變量,如:
for line in io.lines() do
io.write(line, "\n")
end
變量列表的第一元素稱爲“控制變量”。在循環過程中該值絕不會爲nil,因爲當它爲nil時循環就結束了。
for做的第一件事就是對in後面的表達式求值。這些表達式應該返回3個值供for保存:迭代器函數、恆定狀態和控制變量的初值。這裏有點類似與多重賦值,即只有最後一個表達式纔會產生多個結果, 並且只會保留前3個值,多餘的值會被丟棄;而不足的話,將以nil補足。
在初始化步驟之後,for會以恆定狀態和控制變量來調用迭代器函數。然後for將迭代器函數的返回值賦予變量列表中的變量。如果第一個返回值爲nil,那麼循環終止。否則,for執行它的循環體,隨後再次調用迭代器函數,並重復這個過程。
更明確地說,以下語句:
for var_1, ..., var_n in <explist> do <block> end
等價於以下代碼:
do
local _f, _s, _var = <explist>
while true do
local var_1, ..., var_n = _f(_s, _var)
_var = var_1
if _var = =nil then break end
<block>
end
end
因此,假設迭代器函數爲f,恆定狀態爲s,控制變量的初始值爲a0。那麼在循環過程中控制變量的值依次爲a1 = f(s, a0),a2 = f(s, a1),依次類推,直到ai爲nil結束循環。如果for還有其他變量,那麼它們也會在每次調用f後獲得額外的值。

無狀態的迭代器
一種自身不保存任何狀態的迭代器。因此,我們可以在多個循環中使用同一個無狀態的迭代器,避免創建新的closure開銷。
在每次迭代中,for循環都會用恆定狀態和控制變量來調用迭代器函數。一個無狀態的迭代器可以根據這兩個值來爲下次迭代生成下一個元素。這類迭代器的一個典型的例子就是ipairs,它可以用來迭代一個數組的所有元素:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
在這裏,迭代的狀態就是需要遍歷的table(一個恆定狀態,它不會在循環中改變)以及當前的索引值(控制變量)。ipairs(工廠)和迭代器都非常簡單,在Lua中就可以編寫出來:
local function iter (a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
function ipairs (a)
return iter, a, 0
end
當Lua調用for循環中的ipairs(a)時,它會獲得3個值:迭代器函數iter、恆定狀態a和控制變量的初值0。然後Lua調用iter(a, 0),得到1, a[1],在第二次迭代中,繼續調用iter(a, 1),得到2, a[2],依次類推,直到得到第一個nil元素爲止。
函數pairs與ipairs類似,也是用於遍歷一個table中的所有元素。不同的是,它的迭代器函數是Lua中的一個基本函數next。
function pairs (t)
return next, t, nil
end
在調用next(t, k)時,k是table t的一個key。此調用會以table中的任意次序返回一組值:此table的下一個key,以及這個key所對應的值。而調用next(t, nil)時,會返回table的第一組值。若沒有下一組值時,next返回nil。
有些用戶喜歡不通過pairs調用而直接使用next:
for k, v in next ,t do
<body>
end
記住,Lua會自動將for循環中表達式列表的結果調整爲3個值。因此上例中得到 了next、t和nil,這也正與調用pairs(t)的結果完全一致。

具有複雜狀態的迭代器
通常, 迭代器需要保存許多狀態,可是泛型for卻只提供的一個恆定狀態和一個控制變量用於狀態的保存。一個最簡單的解決方法就是使用closure。或者還可以將迭代器所需要的所有狀態打包爲一個table,保存在恆定狀態中。一個迭代器通過這個table就可以保存任意多的數據了。此外,它還能在循環過程中改變這些數據。雖然在循環過程中,恆定狀態總是同一個table(故稱之爲恆定),但這個table的內容卻可以發生改變。由於這種迭代器可以在恆定狀態中保存所有數據,所以他們通常可以忽略泛型for提供的第二個參數(控制變量)。
重寫allword迭代器,將它的狀態保存到一個table中,這個table具有兩個字段:line和pos。
local iterator --在後面定義
function allwords ()
local state = {line = io.read(), pos = 1}
return iterator, state
end

function iterator (state)
while state.line do
local s, e = string.find(state.line, "%w+", state.pos)
if s then
state.pos = e + 1
return string.sub(state.line, s, e)
else
state.line = io.read()
state.pos = 1
end
end
return nil
end

儘可能地嘗試編寫無狀態的迭代器,那些迭代器將所有狀態保存在for變量中,不需要在開始一個循環時創建任何新的對象。如果迭代器無法套用這個模型,那麼就應該嘗試使用closure。closure顯得更加優雅點,通常一個基於closure實現的迭代器會比一個使用table的迭代器更加高效。這是因爲,首先創建一個closure就比創建一個table更廉價,其次訪問“非局部的變量”也比訪問table字段更快。以後會看到另一種使用協同程序編寫迭代器的方式,這種方式是功能最強的,但稍微有一點開銷。

真正的迭代器
“迭代器”這個名稱多少有點誤導的成分。因爲迭代器並沒有做實際的迭代,真正做迭代的是for循環。而迭代器只是爲每次迭代提供一些成功後的返回值。或許,更準確地應稱其爲“生成器”。
還有一種創建迭代器的方法就是,在迭代器中做實際的迭代操作。當使用這種迭代器時,就不需要寫一個循環了。相反,需要一個描述了在每次迭代時需要做什麼的參數,並以此參數來調用迭代器。更確切地說,迭代器接受一個函數作爲參數,並在其內部的循環中調用這個函數。
重寫allwords迭代器:
function allwords (f)
for line in io.lines() do
for word in string.gmatch(line, "%w+") do
f(word) --call the function
end
end
end
使用這個迭代器時,需要傳入一個描述循環體的函數。例如,只想打印每個單詞,那麼可以使用print:
allwords(print)
通常,還可以使用一個匿名函數作爲循環體。例如,以下代碼計算了單詞“hello”在輸入文件中出現的次數:
local count = 0
allwords(function (w)
if w == "hello" then count = count + 1 end
end)
print(count)
"真正的迭代器"在老版本Lua中曾非常流行,那時語言還沒有for語句。




















































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章