在lua種,for語句有兩種形式
- 數值型(numerical)
- 泛型(generic)
數值型for:
基本語法如下
for var exp1, exp2, exp3 do
something
end
在循環開始之前,每個表達式(exp1,exp2,exp3)都會執行一遍,循環開始后就不再執行。
function exp1() print("exp1") return 1 end
function exp2() print("exp2") return 5 end
function exp3() print("exp3") return 1 end
for i = exp1(), exp2(), exp3() do
print(i)
end
輸出:
exp1
exp2
exp3
1
2
3
4
5
var是被for自動聲明的局部變量,初始值為exp1,作用范圍僅限於for循環內部。var的值小於等於exp2之前的每次循環中都會執行something,每次循環結束后都會將步長(exp3)增加到var上,exp3是可選的,如果沒有指定,默認步長為1。如果想跳出循環可以使用break。
如果想用for語句寫出一個無限循環,可與參考如下代碼:
for i = 1, math.huge do
something
end
泛型for:
在了解泛型for之前我們必須了解一下lua中的迭代器和閉包
迭代器(iterator):
迭代器是一種可以讓我們遍歷一個集合中所有元素的代碼結構。在lua種通常使用函數表示迭代器:每次調用函數的時候,函數會返回集合中的下一個元素。
一個典型的例子就是io.read,每次調用的時候它都會返回標准輸入中的下一行,在沒有可以讀取的行的時候返回nil。
閉包:
根據迭代器的概念我們應該能想到,迭代器在連續調用當中應該保存一些狀態,這樣才能知道當前迭代器的位置以及如何從當前位置進步到下一個位置。對於自定義迭代器而言,閉包為保存狀態提供了良好的機制。
閉包就是一個可以訪問自身環境中的局部變量的函數。
這些局部變量記錄了迭代器連續調用過程中的中間值,讓閉包能記住迭代器的位置。當然要創建一個新的閉包,我們還需要非局部變量。因此,一個閉包通常包含兩個函數:
- 閉包本身
- 創建閉包和封裝變量的“工廠”
以下是一個簡單的table迭代器,它遍歷table返回所有的值
function values(t) --創建閉包和封裝變量的“工廠”
local i = 0
print("create iter")
return function() --閉包本身
i = i + 1
print("call iter", i)
return t[i]
end
end
在這個例子中,values作為工廠,每當它被調用的時候,就會創建一個新的閉包(迭代器本身)。這個閉包將自身的狀態保存在外部變量"t"和"i"中,這兩個變量是由values封裝的。每次調用迭代器,i的值加1,然后返回t[i],i作為迭代器的外部變量,在迭代器返回后不會被清除,其值得以保存下來,起到了記錄迭代器位置的作用。在遍歷完table的最后一個元素后,迭代器返回nil,表示迭代結束。
我們可以在一個while循環中使用這個迭代器,它展示了迭代器的完整工作流程:
iter = values({1, 2, 3, 4})
while true do
local v = iter()
if not v then
break
end
print(v)
end
輸出:
create iter
call iter 1
1
call iter 2
2
call iter 3
3
call iter 4
4
call iter 5
使用泛型for,我們可以以更簡潔的代碼實現上述邏輯。
for f in values({1, 2, 3, 4}) do
print(f)
end
為什么泛型for能實現如此簡潔的代碼?下面我們就來探究一下泛型for背后做了哪些事情。
泛型for的標准語法如下:
for var-list in exp-list do
body
end
- var-list:一個或多個變量組成的列表,由逗號分隔
- exp-list:一個或多個表達式組成的列表,同樣由逗號分隔
我們把變量列表(var-list)中的第一個變量稱為控制變量(control variable),其值永遠不會是nil,因為當其值為nil時循環已經結束了。
泛型for要做的第一件事就是對 exp-list 求值,這些表達式應該返回三個值供泛型for保存:
- 迭代函數
- 不可變狀態
- 控制變量初始值
表達式產生的返回值最多保留三個,不足三個則以nil補齊。例如上面的values()工廠只返回了迭代函數,所以不可變狀態和控制變量初始值均為nil。用代碼表示上述過程就是:
-- _f 迭代函數
-- _s 不可變狀態
-- _v 控制變量初始值
local _f, _s, _v = exp-list
然后泛型for把_s和_v作為參數調用_f,如果返回的第一個值(控制變量)為nil,表示循環結束,否則,把_s和新的返回值作為參數,繼續調用_f。其調用過程類似於數列,用偽代碼可以表示為:
local _v1, ... = _f(_s, _v)
local _v2, ... = _f(_s, _v1)
local _v3, ... = _f(_s, _v2)
...
local _vn, ... = _f(_s, _vn-1)
每次循環產生一次調用。這里_f可以返回多個值,但是只有第一個返回值才是控制變量,用來決定循環是否還要繼續。
所以一個形如:
for var_1, var_2, ... in explist do something end
的泛型for語句可以翻譯為:
local _f, _s, _v = exp-list
while true do
local var_1, var_2, ... = _f(_s, _v)
_v = var_1
if not _v then break end
something
end
與上面最原始的迭代器相比較,這里的迭代函數_f有一個顯著的區別,就是_f可以接收參數了,並且參數的值就包含了當前迭代器的狀態,也就是說迭代器自身不需要保存狀態了。因此誕生了一個新的概念:
無狀態迭代器
顧名思義,無狀態迭代器就是一種不需要保存任何狀態的迭代器。因此在多個循環中可以使用同一個迭代器,從而避免了創建閉包的開銷,讓代碼在性能上得到了提升。ipairs就是一個典型的無狀態迭代器。例如:
for i, v in ipairs({"hello", "lua", "for"}) do
print(i, v)
end
我們自己可以用lua實現一個ipairs迭代器
local function my_iter(t, i)
print("call:"..i)
i = i + 1
local v = t[i]
if v then
return i, v
end
end
function my_ipairs(t)
print("init")
return my_iter, t, 0
end
for i, v in my_ipairs({"hello", "lua", "for"}) do
print(i, v)
end
輸出:
init
call:0
1 hello
call:1
2 lua
call:2
3 for
call:3
在上述示例中,調用for循環的第一步就是對my_ipairs求值,_f, _s, _v分別獲得返回值:
_f = my_iter
_s = t
_v = 0
在第一次循環中執行:
i, v = _f(t, _v) --等價於 i, v = my_iter(t, 0)
執行結果為:
i = 1
v = t[1]
緊接着第二次循環,泛型for會把第一次循環中_f(t, _v)的第一個返回值(i)作為控制變量,進行第二次調用:
i, v = _f(t, _v) --等價於 i, v = my_iter(t, 1)
執行結果為:
i = 2
v = t[2]
按照此規律進行迭代,直至i的值為nil。
pairs函數也是一個無狀態迭代器,它調用的是lua中一個基本的迭代函數:next(t, k)。pairs的原型可以描述如下:
function pairs(t)
return next, t, nil
end
next(t, k)的返回值有兩個:
- 隨機次序返回k的下一個鍵,當key==nil的時候返回表中的第一個鍵,當所有表便利完畢,返回nil
- k對應的值,當k==nil的時候返回表中的第一個值
因此我們可以不使用pairs而直接調用next,一樣可以實現遍歷效果
for i, v in next, {"hello", "lua", "for"} do
print(i, v)
end
總結:
- lua中for循環有兩種,數值類for泛型for。
- 迭代器也有兩種,有狀態的和無狀態的。只有在for循環中才能實現無狀態迭代器。