這一篇章的內容
-
深入函數
-
迭代器與泛型for
-
編譯、執(zhí)行與錯誤
-
協同程序(coroutine)
1.深入函數
-
基礎知識
- Lua中,函數是第一類值,和所有其他值一樣都是匿名的,即他們都沒有名稱。當討論一個函數名時(例如print)實際上是討論持有某個函數的變量
第一類值:表示Lua中函數與其他傳統(tǒng)類型的值(數字、字符串)具有相同的權利。函數可以存儲到變量中(無論是全局變量還是局部變量)或table中,可以作為實參傳遞給其他函數,還可以作為其他函數的返回值。
- 函數是由一些表達式創(chuàng)建的,一個函數的定義實際就是一條賦值語句,這條語句創(chuàng)建了一種類型為function的值 ,并把值賦予了一個變量。
function foo (x) retnrn 2*x end
只是一種語法糖,實際為
foo=functio n x) return 2*x end
- 表達式 function (x) <body> end 可視為一種函數的構造式,就像 table的構造式 {} 一樣這種構造使得結果被稱為一個 匿名函數 雖然一般情況下 會將函數賦予全局變量, 即給與一個名稱 但在某些特殊情況下仍然需要用到匿名函數 例如 table.sort ,需要使用者給出排序的具體方法 如
staff = {
{age=1,name="a"},
{age=2,name="b"},
{age=3,name="c"}
}
可以使用
table.sort(staff,function(a,b) return a.age>b.age end)
像sort這樣可以接受另一個函數作為實參的,便是一個高階函數。但高階函數并沒有什么特權,lua中強調將函數視為第一類值,所以高階函數只是一種給予該觀點的應用體現而已
- 將函數儲存在table字段中可以支持許多Lua的高級應用,例如模塊(module)和面向對象編程
-
closure(閉合函數、閉包)
- 若將一個函數寫在另一個函數之內,name這個位于內部的函數便可以訪問外部函數中的局部變量,這項特征稱之為“語法域” 這里寫一個例子
function sortbygrade(name,grades)
table.sort(names,function(n1,n2))
return grade[n1]>grade[n2] --比較年級
end)
end
在這個例子中,內部的匿名函數可以訪問參數grades,而grades是外部函數 sortbygrades的局部變量 而在這個匿名函數的內部,grades既不是局部變量也不是全局變量,稱之為“非局部變量”
簡單地講,一個closure就是一個函數加上該函數所需訪問的所有“非局部變量(upvalue)”,即這個韓式以及一系列這個函數會訪問到的“非局部變量(upvalue)”
從技術上講,Lua中只有closure而不存在“函數”。因為函數本身就是一種特殊的closure
closure在另一種情況中也非常有用。例如在Lua中函數是存儲在普通變量中的,因此可以輕易地重新定義某些函數,甚至是重新定義那些預定義的函數
下面看一個重新定義函數的例子:重新定義函數sin,使其參數能使用角度來代替原來運行的弧度
do
local oldSin=math.sin
local k=math.pi/180
math.sin=function(x)
return oldSin(x*k)
end
end
這段代碼將老版本保存在了一個私有變量之中,現在只能用過新版本的sin才能夠訪問到他。
可以使用同樣的技術來創(chuàng)建一個安全的運行環(huán)境,即所謂的沙盒(sandbox)當執(zhí)行一些未受信任的代碼就需要一個安全的運行環(huán)境,下面我們看一個示例
do
local oldOPen=io.open
local access_Ok=function(filename,mode)
--《檢查訪問權限》
end
io.open=function(filename,mode)
if access_OK(filename,mode) then
return oldOPen(filename,mode)
else
return nil, "access denied"
end
end
end
這個示例的好處是 他在對原方法修改的基礎上,并沒有改動原方法而是以補丁的形式打了上去,并且使外部無法直接訪問到原方法,只能通過新方法通過驗證再去調用。相對于提供一套大而全的解決方案,Lua提供的則是一套元機制,因此可以根據特定的安全需求來創(chuàng)建一個安全的運行環(huán)境
閉包的本質就是每次運行函數時,都會創(chuàng)建一個closure與一個屬于該closure的環(huán)境(一個table),當在這個closure中創(chuàng)建一個新的closure,那么這個closure就會繼承之前closure的環(huán)境,所以數據能夠得以保存。
-
非全局函數
由于函數是一種第一類值,所以函數不僅可以存儲在全局變量中,還可以存儲在table的字段中和局部變量中。我們之前接觸到了幾個將函數存儲在table中的例子,如io.read math.sin 下面我們看幾個示例寫法
lib={}
lib.foo=funtion(x,y) return x+y end
--使用構造式
lib={
foo=function(x,y) return x+y end
}
--另外一種語法定義函數
lib={}
function lib.foo (x,y) return x+y end
只要將一個函數存儲到一個局部變量中,即得到了一個局部函數 也就是說該函數只能在某個特定的作用域中使用。
因為Lua是將每個程序塊作為一個函數來處理的,所以在一個程序塊中聲明的函數就是局部函數,這些局部函數只在該程序塊中可見。
對于局部函數的定義,Lua還支持一種語法糖
local function f (<參數>)
<函數體>
end
在定義遞歸函數時,需要注意將下面是一個錯誤的示范
local fact =tunction(n)
if n==0 then return 1
else return n*fact(n-1) --錯誤,此時fact還未完全聲明定義結束
end
end
當lua編譯到函數體中調用fact(n-1)的地方時,由于局部變量fact的定義未完畢,因此這句表達式其實是調用了一個全局fact(或報錯找不到對象類似的錯誤)而非函數自身。為了解決該問題,可以先定義一個局部變量。然后在定義函數本身下面是正確示例
local fact --正確寫法,先聲明了fact
fact=function(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
由于local function foo(<參數>)<函數體> end只是一種語法糖,展開為
local foo
foo=function(<參數>)<函數體> end
所以 一下遞歸也是正確的
local function fact(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
此種方法只適用于直接遞歸,對于間接遞歸(a->b->a->b...)還是需要先聲明再定義的方法
-
正確的尾調用
Lua中還有一個特性是lua支持尾調用消除。 所謂的尾調用就是一種類似于 goto 的函數調用。當一個函數調用時另一個函數的最后一個動作時,該調用才算是一條尾調用。
function f(x) return g(x) end
也就是說,當f調用完g以后f就無事可做了。在這種情況下,程序就不需要返回那個“尾調用”(f(x)) 所在的函數了 當g返回時,執(zhí)行控制權可以直接返回到調用f的那個點上。是的在進行尾調用 時,不消耗任何的棧空間,這種實現叫做尾調用消除
正確的尾調用消除:不論 n時多大都不會爆棧
function foo (n)
if n>0 then return foo(n-1) end
end
錯誤的尾調用消除
function f(x) g(x) end --錯誤完g(x)后需要將g(x)的返回值舍棄
return g(x) +1 --錯誤,需要做一次加法
return x or g(x) --錯誤,需要對g(x)返回值進行計算
return (g(x)) --錯誤,需要調整g(x)
在lua中 只有 return <func>(<args>)這樣的調用形式才算是一條尾調用
之前提到,一條尾調用好比就是一條 goto 語句 因此,在 lua 中 尾調用 的一大引用就是編寫狀態(tài)機 在各個狀態(tài)切換時 return 該狀態(tài)函數,可消除尾調用而不會出現遞歸層數過多爆棧
尾調用消除的實際原因是在每次進行尾調用的時候,都只是完成一條 goto 語句 而非傳統(tǒng)調用
-
迭代器與泛型for
- 在Lua中, 通常將迭代器表示為函數,每調用一次函數,及返回集合中的“下一個”元素
- 每個迭代器都需要在每次成功調用之間保持一些狀態(tài),而一個closure(閉包)就是一種可以訪問其外部嵌套環(huán)境中的局部變量的函數
- 對于closure而言,這些變量就可用于在成功調用之間的狀態(tài),從而使closure可以記住他在一次便利中所在的位置
當然,為了創(chuàng)建一個新的closure還必須創(chuàng)建他的這些 非局部的變量 因此一個closure結構通常涉及到兩個函數 closure函數和一個用于創(chuàng)建該closure的工廠(factory)函數
這里寫一個簡單地迭代器
function values(t)
local i = 0
return function() i=i+1; return t[i] end
end
每次調用values時,i變回向后移動一位。
我們這里可以在一個while循環(huán)中使用他
--輸出a中所有的元素
local a={1,2,3,4,5,6,7,8,9}
local iter=values(a)
while true do
local element=iter()
if element ==nil then break end
print(element)
end
- 泛型for:
for element in values(t) do
print(element)
end
泛型for 為一次迭代循環(huán)做了所有的簿記工作,他在內部保存了迭代器函數,因此不再需要iter變量。他在每次新迭代是調用迭代器,并在迭代器返回nil時結束循環(huán)
-
泛型for的語義
在上述的例子中,每一次的迭代都會創(chuàng)建一個closure,這些開銷在某些情況下不太容易接受。這是我們希望能通過泛型for的自身來保存迭代器狀態(tài).
泛型for 再循環(huán)過程內部保存了迭代器函數。實際上他保存著三個值,一個迭代器函數,一個恒定狀態(tài),一個控制變量 接下來我們來詳細說明這部分
泛型 for 語法如下
for <var-list> in <exp-list>
<body>
end
其中<var-list>代表一個或多個變量名列表,<exp-list>代表一個或多個表達式的列表 兩者都已逗號分隔 通常變量名和列表均只有一個如
for line in io.line() do
io.wirte(line,"\n")
end
變量列表的第一元素稱為“控制變量” 在循環(huán)過程中該值均不為為nil 因為當他成為nil時循環(huán)就結束了
for做的第一件事情是對in后面的表達式求值,子而寫表達式會返回三個值供for保存:迭代器函數、恒定狀態(tài)和控制變量的初值。(這里有點類似于多重賦值,只有最后一個表達式才會產生多個結果并且只會保留前三個值 多于的被丟棄,不足用nil補足)
在初始化步驟以后,for會以恒定狀態(tài)和控制變量來調用迭代器函數。然后for將迭代器函數的返回值賦予變量列表中的變量。如果第一個返回值為nil,那么循環(huán)終止
更明確的說,以下下語句
for var_1,...var_n int <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,恒定狀態(tài)為s,控制變量的初值為a0 那么在循環(huán)過程中控制變量的值依次為 a1=f(s,a0) a2=f(s,a1) 一詞類推,直到nil結束循環(huán) 如果for還有其他變量,那么他們也會在每次調用f后獲得額外的值
-
無狀態(tài)的迭代器
是一種自身不保留任何狀態(tài)的迭代器,在許多個循環(huán)中使用同一個無狀態(tài)的迭代去,避免創(chuàng)建新的closure開銷
在每次迭代中,for循環(huán)都會用恒定狀態(tài)和控制變量來調用迭代器函數。一個無狀態(tài)的迭代器可以根據這兩個值來為下次迭代生成喜愛個元素,這類迭代器的一個典型例子就是ipairs。
在這里,迭代的狀態(tài)就是需要遍歷的table(一個恒定狀態(tài),他不會在循環(huán)中改變)及當前的索引值(控制變量) ipairs(工廠)和迭代器都非常簡單,我們可以編寫一個示例
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
函數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
<loop body>
end
要注意的是,lua會自動將for循環(huán)中表達式列表的結果調整為三個值
-
具有復雜狀態(tài)的迭代器
通常得帶器需要保存許多狀態(tài),可是泛型for熏制提供一個恒定狀態(tài)和一個控制變量用于狀態(tài)的保存。一個最簡單的解決方法解釋使用closure,或者將迭代器所需的所有狀態(tài)打包成一個table,保存在恒定狀態(tài)中