在Lua語言中,函數是對語句和表達式進行抽象的主要方式。函數既可以用於完成某種特定任務,也可以只是進行一些計算然後返回計算結果。在前一種情況下,我們將一句函數調用視爲一條語句;而在後一種情況下,我們則將函數調用視爲表達式:
print(8*9 , 9/8)
a = math.sin(3) + math.cos(10)
print(os.date())
無論哪種情況,函數調用時都需要使用一對圓括號把參數列表括起來。即使被調用的函數不需要參數,也需要一對空括號()。對於這個規則,唯一的例外就是,當函數只有一個參數且該參數是字符串常量或表構造器時:
print "Hello World" <--> print("Hello World")
dofile "a.lua" <--> dofile('a.lua')
print [[a multi-line message]] <--> print([[a multi-line message]])
f{x = 10 , y = 20} <--> f({x = 10 , y = 20})
type{}
Lua語言也爲面向對象風格的調用提供了一種特殊的語法,即冒號操作符。形如x:foo(x)的表達式意味爲調用對象o的foo方法。
一個Lua程序既可以調用Lua語言編寫的函數,也可以調用C語言編寫的函數。一般來說,我們選擇使用C語言編寫的函數來實現對性能要求更高,或不容易直接通過Lua語言進行操作的操作系統機制等。例如,Lua語言標準庫中所有的函數就都是使用C語言編寫的。不過,無論一個函數是用Lua語言編寫的還是用C語言編寫的,在調用它們時都沒有任何區別。
正如我們已經在其他示例中所看到的,Lua語言中的函數定義的常見語法格式形如:
function add( a )
local sum = 0
for i = 1, #a do
sum = sum + a[i]
end
return sum
end
這種語法中,一個函數定義具有一個函數名、一個參數組成的列表和由一組語句組成的函數體。參數的行爲與局部變量的行爲完全一致,相當於一個用函數調用時轉入的值進行初始化的局部變量。
調用函數時使用的參數個數可以與定義函數時使用的參數個數不一致。Lua語言會通過拋棄多餘參數和將不足的參數設爲nil的方式來調整參數的個數。例如,考慮如下的函數:
function f (a,b) print(a , b) end
其形爲如下:
f() -- nil nil
f(3) -- 3 nil
f(3,4) -- 3 4
f(3,4,5) -- 3 4 (5被丟棄)
雖然這種行爲可能導致編程錯誤,但同樣又是有用的,尤其是對於默認參數的情況。例如,考慮如下遞增全局計數器的函數:
function incCount( n )
n = n or 1
globalCounter = globalCounter + n
end
該函數以1作爲默認實參,當調用無參數的incCount()時,將globalCounter加1。在調用incCount()時,Lua語言首先把參數n初始化爲nil,接下來or表達式又返回了其第二個操作數,最終把n賦成了默認值1。
多返回值
Lua語言中一種與衆不同但又非常有用的特性是允許一個函數返回多個結果。Lua語言中幾個預定義函數就會返回多個值。我們已經接觸過函數string.find,該函數用於在字符串中定位模式。當找到了對應的模式時,該函數會返回兩個索引值:所匹配模式在字符串中初始字符和結尾字符的索引。使用多重賦值可以同時獲取到這兩個結果:
s, e = string.find("hello lua users" , "Lua")
print(s, e) -- 7 9
請記住,字符串的第一個字符的索引值爲1。
Lua語言編寫的函數同樣可以返回多個結果,只需在return關鍵字後列出所有要返回的值即可。例如,一個用於查找序列中最大元素的函數可以同時返回最大值及該元素的位置:
function maximum(a)
local mi = 1
local m = a[mi]
for i = 1, #a do
if a[i] > m then
mi = i ; m = a[i]
end
end
return m, mi
end
print(maximum({8,10,23,12,5})) --23 3
Lua語言根據函數的被調用情況調整返回值的數量。當函數被作爲一條單獨語句調用時,其所有返回值都會被丟棄;當函數被作爲表達式調用時,將只保留函數的第一個返回值。只有當函數調用是一系列表達式中的最後一個表達式時,其所有的返回值才能被獲取到。這裏所謂的“一系列表達式”在Lua中表現爲4種情況:多重賦值、函數調用時傳入的實參列表、表構造器和return語句。爲了分別展示這幾種情況,接下來舉幾個例子:
function foo0() end -- 不返回結果
function foo1() return "a" end -- 返回1個結果
function foo2() return "a", "b" end -- 返回2個結果
在多重賦值中,如果一個函數調用是一系列表達式中的最後一個表達式,則該函數調用將產生儘可能多的返回值以匹配待賦值變量:
x,y = foo2() -- x = "a", y = "b"
x = foo2() -- x = "a", "b"被丟棄
x,y,z = 10, foo2() -- x = 10, y = "a", z = "b"
在多重賦值中,如果一個函數沒有返回值或者返回值個數不夠多,那麼Lua語言會用nil來補充缺失的值:
x,y = foo0() -- x = nil , y = nil
x,y = foo1() -- x = "a" , y = nil
x,y,z = foo2() -- x = "a" , y = "b" , z = nil
請注意,只有當函數調用一系列表達式中的最後一個表達式時才能返回多值結果,否則只能返回一個結果:
x,y = foo2(), 20 -- x = "a", y = 20 ('b'被丟棄)
x,y = foo0(), 20, 30 -- x = nil, y = 20 (30被丟棄)
當一個調用是另一個函數調用的最後一個實參時,第一個函數的所有返回值都會被作爲實參傳給第二個函數。我們已經見到過很多這樣的代碼結構,例如函數print。由於函數print能夠接收可變數量的參數,所以print(g())會打印出g返回的所有結果。
print(foo0()) -- 沒有結果
print(foo1()) -- a
print(foo2()) -- a b
print(foo2(),1) -- a 1
print(foo2() .. "x") -- ax
當在表達式中調用foo2時,Lua語言會把其返回值的個數調整爲1.因此,在上例的最後一行,只有第一個返回值"a"參與了字符串連接操作。
當我們調用f(g())時,如果f的參數是固定的,那麼Lua語言會把g返回值的個數調整成與f的參數個數一致。
表構造器會完整地接收函數調用的所有返回值,而不會調整返回值的個數:
t = {foo0()} -- t = {}
t = {foo1()} -- t = {"a"}
t = {foo2()} -- t = {"a","b"}
不過,這種行爲只有當函數調用是表達式列表中的最後一個時纔有效,在其他位置上的函數總是隻返回一個結果:
t = {foo0(),foo2(),4} -- t[1] = nil, t[2] = "a", t[3] = 4
最後,形如return f()的語句會返回f返回的所有結果:
function foo(i)
if i == 0 then return foo0()
elseif i == 1 then return foo1()
elseif i == 2 then return foo2()
end
end
print(foo(1)) -- a
print(foo(2)) -- a b
print(foo(0)) -- 無結果
print(foo(3)) -- 無結果
將函數調用用一對圓括號括起來可以強制其只返回一個結果:
print(foo0()) -- nil
print(foo1()) -- a
print(foo2()) -- a
應該意識到,return語句後面的內容是不需要加括號的,如果加了括號會導致程序出現額外的行爲。因此,無論f究竟返回幾個值,形如return(f(x))的語句只返回一個值。又是這可能是我們所希望出現的情況,但有時又可能不是。
可變長參數函數
Lua語言中的函數可以是可變長參數函數,即可以支持數量可變的參數。例如,我們已經使用一個、兩個或多個參數調用過函數print。雖然函數print是在C語言中定義的,但也可以在Lua語言中定義可變長參數函數。
下面是一個簡答的示例,該函數返回所有參數的總和:
function add (...)
local s = 0
for _, v in ipairs{...} do
s = s + v
end
return s
end
print(add(3,4,10,25,12)) -- 54
參數列表中的三個點(…)表示該函數的參數是可變長的。當這個函數被調用時,Lua內部會把它所有參數收集起來,我們把這些被收集起來的參數稱爲函數的額外參數。當函數要訪問這些參數時仍需用到三個點,但不同的是此時這三個點是作爲一個表達式來使用的。在上例中,表達式{…}的結果是一個由所有可變長參數組成的列表,該函數會遍歷該列表來累加其中的元素。
我們將三個點組成的表達式稱爲可變長參數表達式,其行爲類似於一個具有多個返回值的函數,返回的是當前函數的所有可變長參數。
實際上,可以通過變長參數來模擬Lua中普遍的參數傳遞機制,例如:
funtion foo (a,b,c)
可以寫成
function foo(...)
local a,b,c = ...
喜歡Perl參數傳遞機制的人可能會更喜歡第二種形式。
形如下列的函數只是將調用它時所傳入的所有參數簡單地返回:
function id (...) return ... end
該函數是一個多值恆等式函數。下列函數的行爲則類似於直接調用函數foo,唯一不同之處是在調用函數foo之前會先打印出傳遞函數foo的所有參數:
function foo1( ... )
print("calling foo:",...)
return foo(...)
end
當跟蹤對某個特定的函數調用時,這個技巧很有用。
接下來再讓我們看另外一個很有用的示例。Lua語言提供了專門用於格式化輸出的函數string.format和輸出文本的函數io.write。我們會很自然地想到把這兩個函數合併爲一個具有可變長參數的函數:
function fwirte(fmt, ...)
return io.wirte(string.format(fmt, ...))
end
注意,在三個點前遊一個固定的參數fmt。具有可變長參數的函數也可以具有任意數量的固定參數,但固定參數必須放在變長參數之前。Lua語言會先將前面的參數賦給固定參數,然後將剩餘的參數作爲可變長參數。
要遍歷可變長參數,函數可以使用表達式{…}將可變長參數放在一個表中,就像add示例中所作的那樣。不過,在某些罕見的情況下,如果可變長參數中包含無效的nil,那麼{…}獲得的表可能不再是一個有效的序列。此時,就沒有辦法在表中判斷原始參數究竟是不是以nil結尾的。對於這種情況,Lua語言提供了函數table.pack。該函數像表達式{…}一樣保存所有的參數,然後將其放在一個表中返回,但是這個表還有一個保存了參數個數的額外字段"n"。例如,下面的函數使用了函數table.pack來檢測參數中是否有nil:
function nonils(...)
local arg = table.pack(...)
for i = 1 , arg.n do
if arg[i] == nil then return false end
end
return true
end
print(nonils(2,3,nil)) -- false
print(nonils(2,3)) -- true
print(nonils()) -- true
print(nonils(nil)) -- false
另一種遍歷函數的可變長參數的方法是使用函數select。函數select總是具有一個固定的參數select,以及數量可變的參數。如果select是數值n,那麼函數select則返回第n個參數後的所有參數;否則,select應該是字符串"#",以便函數select返回額外參數的總數。
print(select(1,"a","b","c")) -- a b c
print(select(2,"a","b","c")) -- b c
print(select(3,"a","b","c")) -- c
print(select("#","a","b","c")) -- 3
通常,我們在需要把返回值個數調整爲1的地方使用函數select,因此可以把select(n,…)認爲是返回第n個額外參數的表達式。
來看一個使用函數select的典型示例,下面是使用該函數的add函數:
function add(...)
local s = 0
for i = 1, select("#",...) do
s = s + select(i , ...)
end
return s
end
對於參數較少的情況,第二個版本的add更快,因爲該版本避免了每次調用時創建一個新表。不過,對於參數較多的情況,多次帶有很多參數調用函數select會超過創建表的開銷,因此第一個版本會更好。
函數table.unpack
多重返回值還涉及一個特殊的函數table.unpack。該函數的參數是一個數組,返回值爲數組內的所有元素:
print(table.unpack{10,20,30}) -- 10 20 30
a,b = table.unpack{10,20,30} -- a = 10, b = 20 , 30被丟棄
顧名思義,函數table.unpack與函數table.pack的功能相反。pack把參數列表轉換成Lua語言中一個真實的列表,而unpack則把Lua語言中的真實的列表轉換成一組返回值,進而可以作爲另一個函數的參數被使用。
unpack函數的重要用途之一體現在泛型調用機制中。泛型調用機制允許我們動態地調用具有任意參數的函數。例如,在IOS C中,我們無法編寫泛型調用的代碼,只能聲明可變長參數的函數或使用函數指針來調用不同的函數。但是,我們仍然不能調用具有可變量參數的函數,因爲C語言中的每一個函數調用的實參個數是固定的,並且每個實參的類型也是固定的。而在Lua語言中,卻可以做到這一點。如果我們想通過數組a傳入可變的參數來調用函數f,那麼可以寫成:
f(table.unpack(a))
unpack會返回a中所有的元素,而這些元素又被用作f的參數。例如,考慮如下的代碼:
print(string.find("hello","ll"))
可以使用如下的代碼動態地構造一個等價的調用:
f = string.find
a = {"hello","ll"}
print(f(table.unpack(a)))
通常,函數table.unpack使用長度操作符獲取返回值的個數,因而該函數只能用於序列。不過,如果有需要,也可以顯示地限制返回元素的範圍:
print(table.unpack({"Sun","Mon","Tue","Wed"},2,3)) -- Mon Tue
雖然預定義的函數unpack是用C語言編寫的,但是也可以利用遞歸在Lua語言中實現:
function unpack(t,i,n)
i = i or 1
n = n or #t
if i <= n then
return t[i],unpack(t,i+1,n)
end
end
在第一次調用該函數時,只傳入一個參數,此時i爲1,n爲序列長度;然後,函數返回t[1]及unpack(t,2,n)返回的所有結果,而unpack(t,2,n)又會返回t[2]及unpack(t,3,n)返回的所有結果,一次類推,直到處理完n個元素爲止。
正確的尾調用
Lua語言中有關函數的另一個有趣的特性是,Lua語言是支持尾調用消除的。這意味着Lua語言可以正確地尾遞歸,雖然尾調用消除的概念並沒有直接涉及遞歸。
尾調用是被當作函數調用使用的跳轉。當一個函數的最後一個動作是調用另一個函數而沒有再進行其他工作時,就行程了尾調用。例如,下例代碼中對函數g的調用就是尾調用:
function f(x) x = x + 1;return g(x) end
當函數f調用完函數g之後,f不再需要進行其他的工作。這樣,當被調用的函數執行結束後,程序就不再需要返回最初的調用者。因此,在尾調用後,程序也就不需要在調用棧中保存有關調用函數的任何信息。當g返回時,程序的執行路徑會直接返回到調用f的位置。在一些語言的實現中,例如Lua語言解釋器,就利用了這個特點,是的進行尾調用時不使用任何額外的棧空間。我們就將這種實現稱爲尾調用消除。
由於尾調用不會使用棧空間,所以一個程序中能夠嵌套的尾調用的數量是無限的。例如,下例函數支持任意的數字作爲參數:
function foo (n)
if n > 0 then
return foo(n - 1)
end
end
該函數永遠不會發生棧溢出。
關於尾調用消除的一個重點就是如何判斷一個調用是尾調用。很多函數之所有不是尾調用,是由於這些函數在調用之後還進行了其他工作。例如,下例中調用g就不是尾調用:
function f(x)
g(x)
end
這個示例的問題在於,當調用完g後,f在返回前還不得不丟棄g返回的所有結果。類似的,以下的所有調用也都不符合尾調用的定義:
return g(x) + 1 -- 必須進行加法
return x or g(x) -- 必須把返回值限制爲1個
return (g(x)) -- 必須把返回值限制爲1個
在lua語言中,只有形如return func(args)的調用纔是尾調用。不過,由於Lua語言會在調用錢對func及其參數求值,所以func及其參數都可以是複雜的表達式。例如,下面的例子就是尾調用:
return x[i].foo(x[j] + a * b, i + j)