內容來自《Lua程序設計(第四版)》18.1節 —— 迭代器和閉包。
迭代器(iterator)是一種可以讓我們遍歷一個集合中所有元素的代碼結構。在 Lua 語言中,通常使用函數表示迭代器:每一次調用函數時,函數會返回集合中的“下一個”元素。一個典型的例子就是 io.read
,每次調用該函數時它都會返回標準輸入中的下一行,子啊沒有可以讀取的行時返回 nil。
所有迭代器都需要在連續的調用之間保存一些狀態,這樣才能知道當前迭代所處的位置及如何從當前位置進到下一個位置。對於函數 io.read
而言,C 語言會將狀態保存在流的結構體中。對於我們自己的迭代器而言,閉包則爲保存狀態提供了一種良好的機制。一個閉包就是一個可以訪問其自身環境中一個或多個局部變量的函數。這些變量將連續調用過程中的值,並將其保存在閉包中,從而使得閉包能夠記住迭代所處位置。
要創建一個新的閉包,必須創建非局部變量。因此,一個閉包結構通常涉及兩個函數:閉包本身和一個用於創建該閉包及其封裝變量的工廠(factory)。
作爲示例,先爲列表編寫一個簡單迭代器,與 ipairs
不同,帶迭代器並不是返回每個元素的索引而是返回元素的值:
function values(t)
local i = 0
return function() i = i + 1; return t[i] end
end
這個例子中,values
就是工廠。每當調用這個工廠時,它就會創建一個新的閉包(即迭代器本身)。這個閉包將它的狀態保存在其外部的變量 t 和 i 中,這兩個變量也是由 values
創建的。每次調用這個迭代器時,就從列表 t 中返回一個值,在遍歷完最後一個元素後,迭代器返回 nil,迭代結束。
這樣的迭代器可以在 while
循環中使用:
t = { 10, 20, 30 }
iter = values(t) -- 創建迭代器
while true do
local element = iter() -- 調用迭代器
if element == nil then break end
print(element)
end
不過,使用泛型 for
更簡單,畢竟泛型 for
就是爲這種迭代器而設計的:
t = { 10, 20, 30 }
for element in values(t) do
print(element)
end
泛型 for
爲一次迭代循環做了所有的記錄工作:它在內部保存了迭代韓素華,因此不需要變量 iter
;它在每次做新的迭代時都會再次調用迭代器,並在迭代器返回 nil 時結束。下面是一個更高級的例子,它可以遍歷來自標準輸入的所有單詞:
function allwords()
local line = io.read() -- 當前行
local pos = 1 -- 在當前行的位置
return function() -- 迭代函數
while line do -- 當還有行時循環
local w, e = string.match(line, "(%w+)()", pos)
if w then -- 發現一個單詞
pos = e -- 下一個位置位於該單詞後
return w -- 返回該單詞
else
line = io.read() -- 沒找到單詞;嘗試下一行
pos = 1 -- 從第一個位置重新開始
end
end
return nil -- 沒有行了,迭代結束
end
end
爲了完成這樣的遍歷,我們需要保存兩個值:當前行的內容(變量line
)及當前行的當前位置(變量pos
)。有了這些數據,我們就可以不斷產生下一個單詞。這個迭代函數的主要部分時調用函數 string.match
,以當前位置作爲起始在當前行中搜索一個單詞。函數使用模式 %w+
來匹配一個單詞(也就是匹配一個或多個字母或數字字符),如果找到了一個單詞,就捕獲並返回這個單詞及該單詞之後的第一個字符的位置(一個空匹配),迭代函數則更i性能當前位置並返回該單詞;否則,迭代器讀取新的一行,然後重複上述搜索過程。在所有的行都被讀取完後,迭代函數返回 nil 以表示迭代結束。
儘管迭代器本身比較複雜,但 allwords
的使用還是很簡單易懂的:
for word in allwords() do
print(word)
end
對於迭代器而言,一種很常見的情況就是:編寫迭代器可能不太容易,但是使用迭代器卻十分簡單,這也不是大問題,因爲使用 Lua 語言編程的最終用戶一般不會去定義迭代器,而是使用那些宿主因應用已經提供的迭代器。