函數(shù)有兩種用途:
- 完成指定任務,此時函數(shù)作為調用語句使用。
- 計算并返回值,此時函數(shù)作為賦值語句的表達式使用。
function fn(arguments-list)
statements-list
end
調用函數(shù)時,如果參數(shù)列表為空,必須使用()表明是函數(shù)調用。
print(os.date())
當函數(shù)只有一個參數(shù)且參數(shù)是字符串或表構造式時,()是可選的。
print "hello world"
dofile "main.lua"
print [[a multi-line message]]
fn{x=10, y=20}
type{}
在Lua中函數(shù)都是function類型的對象
- 可被比較
- 可賦值給一個變量
- 可傳遞給函數(shù)
- 可從函數(shù)中返回
- 可作為table表中的鍵
函數(shù)定義
Lua使用關鍵字function定義函數(shù)
function fn(arg)
-- function body...
end
函數(shù)定義的語法會定義一個全局函數(shù),名為fn,全局函數(shù)本質上是函數(shù)類型的值賦給全局變量。
函數(shù)變量式
fn = function(arg)
-- function body...
end
由于函數(shù)定義本質上是變量賦值,變量的定義總是應放置在變量使用之前,所以函數(shù)的定義也需要放置在函數(shù)調用之前。
local function fn(arg)
-- function body...
end
local fn = function(arg)
-- function body...
end
如果參數(shù)列表為空,必須使用()表明函數(shù)調用。
定義函數(shù)并調用
-- 定義函數(shù)
function fn()
print("hello function")
end
-- 調用函數(shù)
fn()
在定義函數(shù)時要注意
- 利用名字來解釋函數(shù),變量的目的使人通過名字就能看出函數(shù)、變量的作用。
- 每個函數(shù)的長度要盡量控制在一個屏幕內,一樣就能看明白。
- 讓代碼自己說話,最好是不需要注釋。
由于函數(shù)定義等價于變量賦值,因此也可以將函數(shù)名替換為Lua表中的某個字段。
-- 這種形式的函數(shù)定義不能使用local修飾符,因為不存在定義新的局部變量了。
foo.bar = function()
-- function body...
end
function foo.bar()
-- function body ...
end
案例:接收兩個參數(shù),計算加減乘除的結果,并輸出到屏幕。
function fn(i,j)
return i+j, i-j, i*j, i/j, i%j;
end
a,b,c,d,e = fn(10,5)
print(a,b,c,d,e) -- 15 5 50 2.0 0
函數(shù)參數(shù)
按值傳遞
Lua函數(shù)的參數(shù)大部分是按值傳遞的,值傳遞就是調用函數(shù)時,實參把它的值通過賦值運算傳遞給形參,然后形參的改變和實參就沒有關系了。在這個過程中,實參是通過它在參數(shù)表中的位置與形參匹配起來的。
local function swap(x,y)
local tmp = x
x = y
y = tmp
print(x,y)
end
在調用函數(shù)時,若形參個數(shù)和實參個數(shù)不同時,Lua會自動調整實參個數(shù)。調整規(guī)則:
若實參個數(shù)大于形參個數(shù),從左向右,多余的實參被忽略。若實參個數(shù)小于形參個數(shù),從左向右,沒有實參初始化的形參會被初始化為nil。
變長參數(shù)
https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/function_parameter.html
若定義一個函數(shù),參數(shù)個數(shù)不固定,應該怎么辦呢?這就涉及到Lua中函數(shù)的可變參數(shù)。
-- Lua中三個點表示函數(shù)的參數(shù)個數(shù)不確定,可以改變,即可變參數(shù)。
function fn(...)
end
return關鍵字只能出現(xiàn)在語句塊的結尾,也就是說,在end之前,或者是else之前,或者是until之前。
function fn(x)
return x*x*x;
end
n = fn(5)
print(n) -- 125
function fn(x)
if x<10 then
return x*x*x
else
return x*x
end
end
n = fn(5)
print(n)-- 125
函數(shù)基礎
Lua中函數(shù)是一種對語句和表達式進行抽象的主要機制。
Lua中函數(shù)即可完成某項特定的任務,一條函數(shù)調用可視為一條語句。
$ lua
> a = match.sin(3) + math.cos(10)
> print(a)
-0.69795152101659
> print(os.date())
02/04/18 17:42:52
Lua函數(shù)也可以只做一些計算并返回結果,可視為一句表達式。
$ lua
> print(8*9, 9/8)
72 1.125
無論哪種用法都需將參數(shù)放入一對圓括號中。即使調用函數(shù)時沒有參數(shù),也必須寫出一對空括號。對此規(guī)則只有一種特殊的例外情況:一個函數(shù)若只有一個參數(shù),并且此參數(shù)是一個字面量字符串或table構造式,那么圓括號便是可有可無的。
$ lua
> print "hello world"
> dofile "test.lua"
> print [[a multi-line message]]
函數(shù)只有一個參數(shù),且參數(shù)是一個table構造式,則圓括號可有可無。
> f{x=10, y=20}
> f({x=10, y=20})
> type{}
> type({})
Lua為面向對象式的調用也提供了一種特殊的語法 - 冒號操作符。
> obj.foo(obj, arg)
-- 冒號操作符使調用obj.foo時將obj隱含地作為函數(shù)的第一個函數(shù)
> obj:foo(arg)
Lua程序即可使用以Lua編寫的函數(shù),也可調用以C語言編寫的函數(shù)。
function add(params)
local sum=0;
for k,v in ipairs(params) do
sum=sum+v
end
return sum
end
print(add({1,2,3}))
在這種語法中,一個函數(shù)定義具有一個名詞(函數(shù)名)、一些列參數(shù)(參數(shù)表)、一個函數(shù)體(一系列語句)。
形式參數(shù)(parameter)的使用方式與局部變量非常類似,它們是由調用函數(shù)時的實際參數(shù)(argument)初始化的。
調用函數(shù)時提供的實參數(shù)量可與形參數(shù)量不同,Lua會自動調整實參數(shù)量,以匹配參數(shù)表的要求。這項調整與多重賦值(multiple assignment)很相似,即“若實參多于形參,則舍棄多于實參;若實參不足,則多余形參初始化為nil”。
function fn(a,b)
print(a,b);
end
fn(1) -- 1 nil
fn(1,2) -- 1 2
fn(1,2,3) -- 1 2
雖然這種調整行為會導致一些編程錯誤,但它也是很有用的,尤其是對于默認實參的應用。
function incCount(n)
n = n or 1 -- 函數(shù)以1作為默認實參
count = count + n
print(n, count)
end
fn() -- attempt to perform arithmetic on a nil value (global 'count')
多重返回值
Lua函數(shù)具有一項非常與眾不同的特征,允許函數(shù)返回多個結果。
Lua的幾個預定義函數(shù)就是返回多個值的,string.find()用于在字符串中定位一個模式(pattern),該函數(shù)允許在字符串中找到指定的模式,將返回匹配的起始字符和結尾字符的索引。
startstr,endstr = string.find("hello lua", "lua")
print(startstr, endstr) -- 7 9
以Lua編寫的函數(shù)同樣可以返回多個結果,只需在return關鍵字后列出所有的返回值即可。
例如:查找數(shù)組中最大元素并返回該元素的位置
function max(tbl)
local index = 1
local value = tbl[index]
for i,v in ipairs(tbl) do
if(v > value) then
index = i
value = v
end
end
return index,value
end
print(max{9,2,12,8,3,9})-- 3 12
Lua會調整函數(shù)返回值數(shù)量以適應不同的調用情況
- 若將函數(shù)調用作為一條單獨語句時,Lua會丟棄函數(shù)的所有返回值。
- 所將函數(shù)作為表達式的一部分來調用時,Lua只保留函數(shù)的第一個返回值。只有當一個函數(shù)調用是“一系列表達式”中最后一個元素或僅有一個元素時,才能獲得它的所有返回值。
-- test
local tbl = {9,2,12,8,3,9}
local a = max(tbl)
print(a) -- 3
local a,b = max(tbl)
print(a, b) -- 3 12
local a,b,c = max(tbl)
print(a,b,c)-- 3 12 nil
local a,b,c = 1, max(tbl)
print(a,b,c)-- 1 3 12
local a,b,c,d = 1,max(tbl),0
print(a,b,c,d) -- 1 3 0 nil
這里所謂的“一系列表達式”,在Lua中表現(xiàn)為4種情況
- 多重賦值
- 函數(shù)調用時傳入的實參列表
-
table的構造式 -
return語句
在多重賦值中,若函數(shù)調用是最后的或僅有的一個表達式,那么Lua會保留其盡可能多的返回值,用于匹配賦值變量。若函數(shù)沒有返回值或沒有足夠多的返回值,那么Lua會用nil來補充缺失的值。若函數(shù)調用不是一些列表達式的最后一個元素,那么將只產(chǎn)生一個值。
當函數(shù)調用作為另一個函數(shù)調用的最后一個或僅有的實參時,第一個函數(shù)所有返回值都將作為實參傳入第二個函數(shù)。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
print(fn1())
print(fn2()) -- 1
print(fn3()) -- 1 2
print(fn2(),2) -- 1 2
print(fn2()..'x') -- 1x
table構造式可以完整地接收一個函數(shù)調用的所有結果,即不會有任何數(shù)量方面的調整。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
tbl1 = {fn1()} -- 相當于 tbl1={}
tbl2 = {fn2()} -- 相當于 tbl2={1}
tbl3 = {fn3()} -- 相當于 tbl3={1,2}
不過這種行為只有當一個函數(shù)調用作為最后一個元素時才會發(fā)生,而在其他位置上的函數(shù)調用總是只纏身給一個結果值。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
tbl = {fn1(), fn2(), fn3(), 4}
print(tbl[1], tbl[2], tbl[3], tbl[4]) -- nil 1 1 4
最后一種情況是return語句,諸如return fn()這樣的語句將返回fn的所有返回值。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
function fn(i)
if i==1 then return fn1()
elseif i==2 then return fn2()
elseif i==3 then return fn3()
end
end
print(fn(1), fn(2), fn(3)) -- nil 1 1 2
-- 將函數(shù)調用放入一對圓括號中,從而迫使它只返回一個結果。
print((fn(1)), (fn(2)), (fn(3))) -- nil 1 2
注意return語句后面的內容不需要圓括號,在該位置上書寫圓括號會導致不同的行為。
return (fn(3)) -- 只返回一個值,而無關于fn()返回幾個值
關于多重返回值還要介紹一個特殊函數(shù) unpack,unpack接收一個數(shù)組作為參數(shù),并從下標1開始返回該數(shù)組的所有元素。
-- lua5.2+中全局unpack函數(shù)已被移除,并被table.unpack替代。
local unpack = unpack or table.unpack
x,y,z = unpack{1,2,3}
print(x,y,z) -- 1 2 3
unpack的一項重要用戶體現(xiàn)在“泛型調用(generic call)”機制中,泛型調用機制可動態(tài)地以任何實參來調用任何函數(shù)。
-- lua5.2+中全局unpack函數(shù)已被移除,并被table.unpack替代。
local unpack = unpack or table.unpack
fn = string.find
tbl = {'hello lua', 'lua'}
print(fn(unpack(tbl))) -- 7 9
變長參數(shù)
Lua函數(shù)可接受可變數(shù)量的參數(shù),和C語言類似。在函數(shù)參數(shù)列表中使用...表示函數(shù)有可變的參數(shù)。
Lua將函數(shù)的參數(shù)放在一個叫做arg的表中,除了參數(shù)外,arg表中還有一個域n表示參數(shù)的個數(shù)。
function dump(...)
local str = ""
for i,v in ipairs(arg) do
str = str .. tostring(v).."\t"
end
return str.."\n"
end
例如:只想要string.find返回的第二個值,典型的方法是使用虛變量_。
local _, x = string.find(str, pattern)
Lua中的函數(shù)還可以接受不同數(shù)量的實參
例如:返回所有參數(shù)的總和
function sum(...)
local sum=0
for i,v in ipairs{...} do
sum = sum + v
end
return sum
end
print(sum(1,2,3,4)) -- 10
參數(shù)中3個點...表示函數(shù)可接收不同數(shù)量的實參,當函數(shù)被調用時,它的所有參數(shù)都會被收集到一起。這部分收集起來的實參稱為函數(shù)的“變長參數(shù)(variable arguments)”。函數(shù)要訪問它的變長參數(shù)時,仍需要3個點。但不同的是,此時這個3個點是作為一個表達式來使用的。
function fn(...)
local x,y,z = ...
print(x,y,z)
end
fn(1,2) -- 1 2 nil
fn(1,2,3) -- 1 2 3
表達式...的行為類似于一個具有多重返回值的函數(shù),它返回的是當前函數(shù)的所有變量參數(shù)。
function fwrite(fmt, ...)
return io.write(string.format(fmt, ...))
end
fwrite("%s %s %s", 1, 2, 3) -- 1 2 3
Lua中迭代函數(shù)參數(shù)時,可使用...參數(shù)收集到一個table中,但變參中函數(shù)非法的nil時,可使用select函數(shù)將其過濾掉。
function filter(...)
for i=1, select("#", ...) do
local arg = select(i, ...)
if arg ~= nil then
print(arg)
end
end
end
-- test
test(1,nil,2,3)-- 1 2 3
具名實參
Lua的函數(shù)參數(shù)是和位置相關的,調用時會按順序依次傳遞給形式參數(shù)。有時候,使用名字制定參數(shù)是很有用的(命名參數(shù))。
Lua中的參數(shù)傳遞機制和是具有“位置性”的,也就是說在調用函數(shù)時,實參時通過它在參數(shù)表中的位置與形參匹配起來。
function rename(tbl)
print(tbl, tbl.oriname, tbl.newname)
end
rename{oriname="ori.lua", newname="new.lua"}
Lua中特殊的函數(shù)調用語法,當實參只有一個table構造式時,函數(shù)調用中的圓括號是可有可無的。
function rename(arg)
return os.rename(arg.old, arg.new)
end
第一類值
Lua中的函數(shù)是帶有詞法定界(lexical scoping)的第一類值(first-class values)。
"第一類值"是指在Lua中函數(shù)和其他值(數(shù)值、字符串...)一樣,函數(shù)可以被存儲在變量中,可以存放在表中,可以作為函數(shù)的參數(shù),也可以作為函數(shù)的返回值。
“詞法界定”是指被嵌套的函數(shù)可以訪問其他外部函數(shù)中的變量,這一特性給Lua提供了強大的編程能力。
Lua中關于函數(shù)難以理解的是“函數(shù)是可以沒有名字的,也就是匿名的?!保岬胶瘮?shù)名時,實際上是說以一個指向函數(shù)的變量,和其他類型值得變量是一樣的。
Lua中函數(shù)作為“第一類值”,表示函數(shù)可以存儲在變量中,可通過參數(shù)傳遞給其他函數(shù),還可作為其他函數(shù)的返回值。由于函數(shù)在Lua中是一種“第一類值”,所以不僅可將其存儲在全局變量中,還可存儲在局部變量甚至table的字段中。
Lua中函數(shù)是一種“第一類值(First-Class Value)”,它們具有特定的詞法域(Lexical Scoping)。
“第一類值”是什么意思呢?這表示在Lua中函數(shù)與其他傳統(tǒng)類型的值具有相同的權利。函數(shù)可存儲到變量中或table中,可作為實參傳遞給其他函數(shù),還可作為其他函數(shù)的返回值。
“詞法域”是什么意思呢?這是指一個函數(shù)可以嵌套在另一個函數(shù)中,內部的函數(shù)可訪問外部函數(shù)的變量。這項聽起來平凡的特性將給語言帶來極大的能力,因為它允許在Lua中應用各種函數(shù)式語言(functional-language)中強大的編程技術。
a = {p = print}
a.p('hello') -- hello
print = math.sin
a.p(print(1)) -- 0.8414709848079
sin = a.p
sin(10,20) -- 10 20
Lua對函數(shù)式編程(functional programming)提供了良好的支持
在Lua中有一個很容易混淆概念是,函數(shù)與所有其他值一樣都是匿名的,即它們都沒有名稱。當討論一個函數(shù)名時,實際上是在討論一個持有某函數(shù)的變量。這與其他變量持有各種值的一個道理,可以以多種方式來操作這些變量。
如果說函數(shù)是值的話,那是否可以說函數(shù)就是由一些表達式創(chuàng)建的呢?是的,事實上在Lua中最常見的是函數(shù)編寫方式。
function fn(x)
return 2*x
end
-- 簡化書寫“語法糖”
fn = function(x) return 2*x end
一個函數(shù)定義實際就是一條語句,更準確地說是一條賦值語句,這條語句創(chuàng)建了一種類型為“函數(shù)”的值。
可將表達式function(x) <body> end視為一種函數(shù)的構造式,類似table的構造式{}一樣。將這種函數(shù)構造式的結果稱為一個“匿名函數(shù)”,雖然一般情況下,會將函數(shù)賦予全局變量,即給予其一個名稱。但在某些特殊情況下,仍會需要用到匿名函數(shù)。
Lua即可以調用以自身Lua語言編寫的函數(shù),又可以調用以C語言編寫的函數(shù)。
table庫提供了一個函數(shù)table.sort,它接收一個table并對其中的元素排序。向這種函數(shù)就必須支持各種各樣可能的排序規(guī)則。sort函數(shù)并沒有提供所有排序準則,而是提供了一個可選的參數(shù),所謂“次序函數(shù)(order function)”。這個函數(shù)接收兩個元素,并返回在有序情況下第一個元素是否已排在第二個元素之前。
local tbl = {
{name="ip1", ip="210.26,30,34"},
{name="ip2", ip="210.26,30,33"},
{name="ip3", ip="210.26,30,12"},
}
table.sort(tbl, function(x,y)
return x.name > y.name
end)
for k,v in pairs(tbl) do
print(v.name) --ip3 ip2 ip1
end
像sort這樣的函數(shù),接收另一個函數(shù)作為實參,稱其為“高階函數(shù)(higher-order function)”。高階函數(shù)是一種強大的編程機制,應用匿名函數(shù)來創(chuàng)建高階函數(shù)所需的實參則可以帶來更大的靈活性。但請記住,高階函數(shù)并沒有什么特權。Lua強調將函數(shù)視為“第一類值”,所以高階函數(shù)只是一種基于該觀點的應用體現(xiàn)而已。
例如:關于導數(shù)的高階函數(shù)
function derivative(fn, delta)
delta = delta or 1e-4
return function(x)
return (fn(x+delta)-fn(x))/delta
end
end
fn = derivative(math.sin)
print(math.cos(10), fn(10)) // -0.83907152907645 -0.83904432662041
閉包函數(shù)
當一個函數(shù)內部嵌套另一個函數(shù)定義時,內部的函數(shù)體可以訪問外部函數(shù)的局部變量,這種特征稱之為“詞法界定”。詞法界定加上第一類函數(shù)在編程語言中是一個功能強大的工具。
若將函數(shù)寫在另一個函數(shù)何內,那么這個位于內部的函數(shù)便可訪問外部函數(shù)中的局部變量,這項特征稱之為“詞法域”。
例如:根據(jù)每個學生的年級來對它們姓名進行由高到低的排序
local userlist = {
{username="mary", score=81},
{username="shiva", score=92},
{username="seth", score=65}
}
table.sort(userlist, function(x, y)
return x.score > y.score
end)
for k,v in pairs(userlist) do
print(k, v.username, v.score)
end
--[[
1 shiva 92
2 mary 81
3 seth 65
--]]
創(chuàng)建函數(shù)完成操作
function sort_by_grade(names, grades)
table.sort(names,
function(a,b)
return grades[a] > grades[b]
end
);
end
names = {"alice", "peter", "paul", "mary"}
grades = {alice=10, peter=5, paul=9, mary=2}
sort_by_grade(names,grades)
for key,val in pairs(names) do
print(key, val, grades[val])
end
有趣的是,傳遞給sort匿名函數(shù)可以訪問參數(shù)grades,而grades是外部函數(shù)sort_by_grade的局部變量。在這個匿名函數(shù)內部,grades即不是全局變量也不是局部變量,將其稱為一個非局部的變量(non-local variable)或upvalues或“外部的局部變量(external local variable)”。為什么在Lua中允許這種訪問呢?原因在于函數(shù)是“第一類值”。
function count()
local i = 0
return function()
i = i + 1
return i
end
end
cnt = count()
print(cnt()) -- 1
print(cnt()) -- 2
print(cnt()) -- 3
匿名函數(shù)訪問了一個“非局部的變量i,i變量用于保持一個計數(shù)器。Lua會以closure的概念來正確地處理這種情況。簡單來說,一個closure就是一個函數(shù)加上該函數(shù)所需訪問的所有“非局部的變量”。如果再次調用count(),那么它會創(chuàng)建一個新的局部變量i,從而也將得到一個新的closure。
技術上來講,閉包指的是值而不是函數(shù),函數(shù)僅僅是閉包的原型聲明,盡管如此,在不會導致混淆的情況下,使用術語函數(shù)代指閉包。
從技術上講,Lua中只有closure,而不存在函數(shù)。因此,函數(shù)本身就是一種特殊的closure。不過只要不會引起混淆,仍將采用術語函數(shù)來指代closure。
在許多場合中closure都是一種很有價值的工具,例如:可作為sort類高階函數(shù)的參數(shù)。closure對于創(chuàng)建其他函數(shù)也很有價值。這種機制使Lua可混合在函數(shù)式編程世界中久經(jīng)考驗的編程技術。另外closure對于回調函數(shù)也很有用。
例如,假設有一個傳統(tǒng)的GUI工具包可以創(chuàng)建按鈕,每個按鈕都有一個回調函數(shù),每當用戶按下按鈕是GUI工具包都會調用這些回調函數(shù)。再假設,基于此要做一個十進制計算器,其中需要10個數(shù)字按鈕,你會發(fā)現(xiàn)這些按鈕之間的區(qū)別其實并不大,僅需在按下不同按鈕時稍微不同的操作就可以了。
-- 創(chuàng)建按鈕
function digitButton(digit)
return Button{label=tostring(digit), action=function() add_to_display(digit) end}
end
假設Button是工具包中一個用于創(chuàng)建新按鈕的函數(shù),label是按鈕的標簽,action是回調closure,每當按鈕按下時就會調用它?;卣{一般發(fā)生在digitButton函數(shù)執(zhí)行完后,那時局部變量digit已經(jīng)超出了作用范圍,但closure仍可以訪問到這個變量。
closure在另一種情況中也非常有用,例如在Lua中函數(shù)是存儲在普通變量中的,因此可以輕易地重新定義某些函數(shù),甚至是重新定義那些預定義的函數(shù)。這也是Lua相當靈活的原因之一。
通常當重新定義一個函數(shù)時,需要在新的實現(xiàn)中調用原來的那個函數(shù)。舉例來說,假設要重新定義函數(shù)sin,使其參數(shù)能使用角度來替代原先的弧度。那么這個新函數(shù)就必須得轉換它的實參,并調用原來的sin函數(shù)完成真正的計算。
oldSin = math.oldSin
math.sin = function(x)
return oldSin(x*math.pi/180)
end
還有一種更徹底的做法
do
local oldSin = math.sin
local k = math.pi/180
math.sin = function(x)
return oldSin(x*k)
end
end
將老版本的sin保存到一個私有變量,現(xiàn)在只有通過新版本的sin才能訪問到它。
可以使用同樣的技術來創(chuàng)建一個安全的運行環(huán)境,即所謂的沙盒(sandbox)。當執(zhí)行一些未受信任的代碼時就需要一個安全的運行環(huán)境,例如在服務器中執(zhí)行那些從Internet上接收到的代碼。
舉例來說,如果要限制一個程序訪問文件的話,只需要使用closure來重新定義函數(shù)io.open()即可。
do
local oldOpen = io.open
local accessOK = function(filename,mode)
-- 檢查訪問權限
end
io.open =function(filename, mode)
if accessOK(filename, mode) then
return oldOpen(filename,mode)
else
return nil,"access denied"
end
end
end
經(jīng)過重新定義后,一個函數(shù)就只能通過受限版本來調用原來那個未受限的open()函數(shù)了。將原來不安全的版本保存到closure的一個私有變量中,從而使得外部再也無法直接訪問到原來的版本了。
通過這種技術,可以再Lua的語言層面上就構建出一個安全的運行環(huán)境,且不失建議性和靈活性。相對于提供一套大而全的解決方案,Lua提供的則是一套“元機制(meta-mechanism)”,因此可以根據(jù)特定的安全需要來創(chuàng)建一個安全的運行環(huán)境。
非全局的函數(shù)
由于函數(shù)是一種“第一類值”,因此一個顯而易見的推論是,函數(shù)不僅可以存儲在全局變量中,也可以存儲在table的字段中和局部變量中。
Lua中大部分庫采用將函數(shù)存儲在table字段中這種機制。若要在Lua中創(chuàng)建這種函數(shù),只需將常規(guī)的函數(shù)語法和table語法結合即可。
Lib = {}
Lib.foo = function(x,y)
return x+y
end
Lib.bar = function(x,y)
return x-y
end
當然,也可使用構造式:
Lib = {
foo=function(x,y) return x+y end,
bar=function(x,y) return x-y end
}
只要將一個函數(shù)存儲到一個局部變量中,即得到一個局部函數(shù)(local function),也就是說該函數(shù)只能在某個特定的作用域中使用。
對于程序包(package)而言,這種函數(shù)定義是非常有用的,因為Lua是將每個特定程序塊(chunk)作為一個函數(shù)來處理的,所以在一個程序塊中聲明的函數(shù)就是局部函數(shù),這些局部函數(shù)只在該程序塊中可見。詞法域確保了程序包中的其他函數(shù)可以使用這些局部函數(shù)。
local fn = function(arg)
-- function body
end
local func = function(arg)
fn()
end
對于局部函數(shù)的定義,Lua支持一種特殊的語法糖:
local function fn(arg)
-- function body
end
在定義遞歸的局部函數(shù)中,有一個特別之處需要注意。
local face = function(n)
if n==0 then
return 1
else
-- 錯誤的遞歸調用
-- 當Lua編譯到函數(shù)體中調用fact(n-1)的地方時,由于局部的fact尚未定義完畢。
-- 因此這句表達式其實調用了一個全局的fact,而非此函數(shù)本身。
return n*face(n-1)
end
end
--[[結局方案:可以先定義一個局部變量,然后再定義函數(shù)本身。--]]
local fact
fact = function(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
--[[
現(xiàn)在函數(shù)中的fact調用就表示了局部變量,即使在函數(shù)定義時,這個局部變量的值尚未完成定義,但之后在函數(shù)執(zhí)行時,fact則肯定擁有了正確的值。
--]]
當Lua展開局部函數(shù)定義的“語法糖”時,并不是使用基礎函數(shù)定義語法。而是對于局部函數(shù)定義:
local function fn(<args>) <function body> end
Lua將其展開為
local fn
fn = function(<args>) <function body> end
因此,使用此種語法定義遞歸函數(shù)不會產(chǎn)生錯誤:
local function fact(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
當然,這個技巧對于間接遞歸的函數(shù)而言是無效的。對于間接遞歸的情況下,必須使用一個明確的向前聲明(Forward Declaration):
local fn,func -- 向前聲明
function func()
fn()
end
function fn()
func()
end
--[[
注意,別把第二個函數(shù)定義為"local function fn"。
如果那樣的話,Lua會創(chuàng)建一個全新的局部變量fn。
而將原來聲明的fn(func函數(shù)中所引用的那個)置于未定義狀態(tài)。
--]]
尾調用
Lua函數(shù)有一個有趣的特征,那就是Lua支持“尾調用消除(tail-call elimination)”。
所謂“尾調用(tail call)”就是一種類似于goto()的函數(shù)調用,當一個函數(shù)調用是另一個函數(shù)的最后一個動作時,該調用才算是一條“尾調用”。
function fn(x)
return func(x)
end
--[[
當fn()函數(shù)調用完func()函數(shù)之后就再無其他事情可做了
因此在這種情況下,程序就不需要返回那個“尾調用”所在的函數(shù)了。
所以在“尾調用”之后,程序也不需要保存任何關于該函數(shù)的棧(stack)信息。
當func()函數(shù)返回時,執(zhí)行控制權可以直接返回給調用fn()函數(shù)的那個點上。
有些語言實現(xiàn)(例如Lua解釋器)可以得益于這個特點,使得在進行尾調用時不耗費任何棧空間。
將這種實現(xiàn)稱之為支持“尾調用消除”。
--]]
由于“尾調用”不會耗費??臻g,所以一個程序可以擁有無數(shù)嵌套的“尾調用”。
function fn(n)
if n>0 then
return fn(n-1)
end
end
--[[
在調用fn()函數(shù)時,傳入任何數(shù)字n作為參數(shù)都不會造成棧溢出。
--]]
有一點需要注意的是,當想要受益于“尾調用消除”時,務必要確定當前的調用時一條“尾調用”。判斷的準則是“一個函數(shù)在調用完另一個函數(shù)之后,是否就無其他事情需要做了”。有些看似“尾調用”的代碼,其實都違背了這條準則。
function fn(x)
func(x)
end
--[[
當調用完func()函數(shù)后,fn()函數(shù)并不能立即返回,它還需丟棄func()函數(shù)返回的臨時結果。
--]]
Lua中,只有return <func>(<args>)這樣的調用形式才算是一條“尾調用”。
return fn(x)+1 -- 必須做一次加法
return x or fn(x) -- 必須調整為一個返回值
return (g(x)) -- 必須調整為一個返回值
Lua在調用前對<func>及其參數(shù)求值,所以它們可以是任意復雜的表達式。
return x[i].fn(x[j]+a*b, i+j)
一條“尾調用”就好比是一條goto語句。因此,在Lua中“尾調用”的一大應用就是編寫“狀態(tài)機(state machine)”。這種程序通常以一個函數(shù)來表示一個狀態(tài),改變狀態(tài)就是goto(或調用)到另一個特定的函數(shù)和。
例如:一個簡單的迷宮游戲中,一個迷宮有幾間房間,每間房中最多有東南西北4扇門。用戶在每一步異動中都需要輸入一個移動的方向。如果在某個方向上有門,那么用戶可以進入相應的房間。不然,程序就打印一條警告。游戲的目標就是讓用戶從最初的房間走到最終的房間。
這個游戲就是典型的狀態(tài)機,其中當前房間就是一個狀態(tài)。可以將迷宮的每間房實現(xiàn)為一個函數(shù),并使用“尾調用”來實現(xiàn)從一間房移動到另一件。
function room1()
local move = io.read()
if move=='south' then
return room3()
elseif move=="east" then
return room2()
else
print("invalid move")
return room1()
end
end
function room2()
local move = io.read()
if move=='south' then
return room4()
elseif move=='west' then
return room1()
else
print('invalid move')
return room2()
end
end
function room3()
local move=io.read()
if move=='north' then
return room1()
elseif move=='ease' then
return room4()
else
print("invalid move")
return room3()
end
end
function room4()
print("congratulations")
end
-- 調用初始房間來開始游戲
room1()
--[[
若沒有“尾調用消除”,每次用戶的異動都會創(chuàng)建一個新的棧層(stack level)
異動若干步后就有可能會導致棧溢出
“尾調用消除”則對用戶異動的次數(shù)沒有任何限制
因為每次異動實際上都只是完成一條goto語句到另一個函數(shù),而非傳統(tǒng)的函數(shù)調用。
--]]
對于簡單的游戲而言,或許覺得將程序設計為數(shù)據(jù)驅動的會更好一些,其中將房間和異動記錄在table中。不過,如果游戲中每間房間都有各自特殊的情況的話,采用這種狀態(tài)機的設計則更為合適。
Lua迭代器與閉包
迭代器是一種可以遍歷(iterate over)集合中所有元素的機制。在Lua中通常將迭代器表示為函數(shù),每次調用函數(shù)即返回集合中的“下一個”元素。
每個迭代器都需要在每次成功調用之間保持一些狀態(tài),這樣才能知道它所在的位置及如何步進到下一個位置。閉包(closure)對于這類任務提供極佳的支持,一個閉包就是一種可以訪問其外部嵌套環(huán)境中的局部變量的函數(shù)。
對于閉包而言,這些變量就可用于在成功調用之間保持狀態(tài)值,從而使閉包可以記住它在一次遍歷中所在的位置。
當然,為了創(chuàng)建一個新的閉包,還必須創(chuàng)建它的“非局部變量(non-local variable)”。因此,一個閉包結構通常涉及到兩個函數(shù):閉包本身和一個用于創(chuàng)建該閉包的工廠函數(shù)。
-- 為列表編寫簡單的迭代器,與ipaires不同的是該迭代器并不是返回每個元素的索引,而是返回元素的值。
-- values是一個工廠,每當調用這個工廠時,就創(chuàng)建一個新的閉包(迭代器本身)。這個閉包將它的狀態(tài)保存在其外部變量tbl和i中。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 每當調用這個迭代器時,它就從列表tbl中返回下一個值。
-- 直到最后一個元素返回后,迭代器就會返回nil,以此表示迭代器的結束。
tbl={1,2,3,4}
iter = values(tbl) -- 創(chuàng)建迭代器
while true do
local el = iter() -- 調用迭代器
if el==nil then
break
end
print(el)
end
然而,使用泛型for則更為簡單,你會發(fā)現(xiàn)泛型for正是為這種迭代而設計的。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 泛型for為一次迭代循環(huán)做了所有的薄記工作。
-- 它在內部保存了迭代器函數(shù),因此不再需要iter變量。
-- 它在每次新迭代時調用迭代器,并在迭代器返回nil時結束循環(huán)。
tbl={1,2,3,4}
for el in values(tbl) do
print(el)
end
需求:遍歷當前輸入文件中所有單詞的迭代器
為完成這樣的遍歷,需要保持兩個值:當前行的內容、當前行所處的位置。有了這些信息就可以不斷產(chǎn)生下一個單詞。迭代器函數(shù)的主要部分使用 string.find在當前行中以當前位置作為起始來所搜索一個單詞。使用模式%w+來描述一個單詞,它用于匹配一個或多個的文字/數(shù)字字符。如果string.find找到了一個單詞,迭代器就會將當前位置更新為該單詞之后的第一個字符,并返回該單詞。否認則,它就讀取新的一行并反復這個搜索過程。若沒有剩余的行,則返回nil表示迭代的結束。
function allwords()
local line = io.read()
local pos = 1
return function()
while line do
local s,e = string.find(line, "%w+", pos)
if s then
pos = e+1
return string.sub(line,s,e)
else
line = io.read()
pos = 1
end
end
return nil
end
end
for word in allwords() do
print(word)
end