函數(shù)有兩種用途:
- 完成指定任務(wù),此時(shí)函數(shù)作為調(diào)用語(yǔ)句使用刊棕。
- 計(jì)算并返回值,此時(shí)函數(shù)作為賦值語(yǔ)句的表達(dá)式使用待逞。
function fn(arguments-list)
statements-list
end
調(diào)用函數(shù)時(shí)甥角,如果參數(shù)列表為空,必須使用()
表明是函數(shù)調(diào)用识樱。
print(os.date())
當(dāng)函數(shù)只有一個(gè)參數(shù)且參數(shù)是字符串或表構(gòu)造式時(shí),()
是可選的嗤无。
print "hello world"
dofile "main.lua"
print [[a multi-line message]]
fn{x=10, y=20}
type{}
在Lua中函數(shù)都是function
類型的對(duì)象
- 可被比較
- 可賦值給一個(gè)變量
- 可傳遞給函數(shù)
- 可從函數(shù)中返回
- 可作為table表中的鍵
函數(shù)定義
Lua使用關(guān)鍵字function
定義函數(shù)
function fn(arg)
-- function body...
end
函數(shù)定義的語(yǔ)法會(huì)定義一個(gè)全局函數(shù),名為fn
怜庸,全局函數(shù)本質(zhì)上是函數(shù)類型的值賦給全局變量当犯。
函數(shù)變量式
fn = function(arg)
-- function body...
end
由于函數(shù)定義本質(zhì)上是變量賦值,變量的定義總是應(yīng)放置在變量使用之前割疾,所以函數(shù)的定義也需要放置在函數(shù)調(diào)用之前嚎卫。
local function fn(arg)
-- function body...
end
local fn = function(arg)
-- function body...
end
如果參數(shù)列表為空,必須使用()
表明函數(shù)調(diào)用宏榕。
定義函數(shù)并調(diào)用
-- 定義函數(shù)
function fn()
print("hello function")
end
-- 調(diào)用函數(shù)
fn()
在定義函數(shù)時(shí)要注意
- 利用名字來(lái)解釋函數(shù)拓诸,變量的目的使人通過(guò)名字就能看出函數(shù)、變量的作用麻昼。
- 每個(gè)函數(shù)的長(zhǎng)度要盡量控制在一個(gè)屏幕內(nèi)奠支,一樣就能看明白。
- 讓代碼自己說(shuō)話抚芦,最好是不需要注釋倍谜。
由于函數(shù)定義等價(jià)于變量賦值,因此也可以將函數(shù)名替換為L(zhǎng)ua表中的某個(gè)字段叉抡。
-- 這種形式的函數(shù)定義不能使用local修飾符尔崔,因?yàn)椴淮嬖诙x新的局部變量了。
foo.bar = function()
-- function body...
end
function foo.bar()
-- function body ...
end
案例:接收兩個(gè)參數(shù)褥民,計(jì)算加減乘除的結(jié)果季春,并輸出到屏幕。
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ù)大部分是按值傳遞的轴捎,值傳遞就是調(diào)用函數(shù)時(shí)鹤盒,實(shí)參把它的值通過(guò)賦值運(yùn)算傳遞給形參,然后形參的改變和實(shí)參就沒(méi)有關(guān)系了侦副。在這個(gè)過(guò)程中侦锯,實(shí)參是通過(guò)它在參數(shù)表中的位置與形參匹配起來(lái)的。
local function swap(x,y)
local tmp = x
x = y
y = tmp
print(x,y)
end
在調(diào)用函數(shù)時(shí)秦驯,若形參個(gè)數(shù)和實(shí)參個(gè)數(shù)不同時(shí)尺碰,Lua會(huì)自動(dòng)調(diào)整實(shí)參個(gè)數(shù)。調(diào)整規(guī)則:
若實(shí)參個(gè)數(shù)大于形參個(gè)數(shù)译隘,從左向右亲桥,多余的實(shí)參被忽略。若實(shí)參個(gè)數(shù)小于形參個(gè)數(shù)固耘,從左向右题篷,沒(méi)有實(shí)參初始化的形參會(huì)被初始化為nil
。
變長(zhǎng)參數(shù)
https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/function_parameter.html
若定義一個(gè)函數(shù)厅目,參數(shù)個(gè)數(shù)不固定番枚,應(yīng)該怎么辦呢?這就涉及到Lua中函數(shù)的可變參數(shù)损敷。
-- Lua中三個(gè)點(diǎn)表示函數(shù)的參數(shù)個(gè)數(shù)不確定葫笼,可以改變,即可變參數(shù)拗馒。
function fn(...)
end
return
關(guān)鍵字只能出現(xiàn)在語(yǔ)句塊的結(jié)尾路星,也就是說(shuō),在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ù)基礎(chǔ)
Lua中函數(shù)是一種對(duì)語(yǔ)句和表達(dá)式進(jìn)行抽象的主要機(jī)制挥等。
Lua中函數(shù)即可完成某項(xiàng)特定的任務(wù)垫挨,一條函數(shù)調(diào)用可視為一條語(yǔ)句。
$ lua
> a = match.sin(3) + math.cos(10)
> print(a)
-0.69795152101659
> print(os.date())
02/04/18 17:42:52
Lua函數(shù)也可以只做一些計(jì)算并返回結(jié)果触菜,可視為一句表達(dá)式九榔。
$ lua
> print(8*9, 9/8)
72 1.125
無(wú)論哪種用法都需將參數(shù)放入一對(duì)圓括號(hào)中。即使調(diào)用函數(shù)時(shí)沒(méi)有參數(shù)涡相,也必須寫(xiě)出一對(duì)空括號(hào)哲泊。對(duì)此規(guī)則只有一種特殊的例外情況:一個(gè)函數(shù)若只有一個(gè)參數(shù),并且此參數(shù)是一個(gè)字面量字符串或table構(gòu)造式催蝗,那么圓括號(hào)便是可有可無(wú)的切威。
$ lua
> print "hello world"
> dofile "test.lua"
> print [[a multi-line message]]
函數(shù)只有一個(gè)參數(shù),且參數(shù)是一個(gè)table構(gòu)造式丙号,則圓括號(hào)可有可無(wú)先朦。
> f{x=10, y=20}
> f({x=10, y=20})
> type{}
> type({})
Lua為面向?qū)ο笫降恼{(diào)用也提供了一種特殊的語(yǔ)法 - 冒號(hào)操作符缰冤。
> obj.foo(obj, arg)
-- 冒號(hào)操作符使調(diào)用obj.foo時(shí)將obj隱含地作為函數(shù)的第一個(gè)函數(shù)
> obj:foo(arg)
Lua程序即可使用以Lua編寫(xiě)的函數(shù),也可調(diào)用以C語(yǔ)言編寫(xiě)的函數(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}))
在這種語(yǔ)法中棉浸,一個(gè)函數(shù)定義具有一個(gè)名詞(函數(shù)名)、一些列參數(shù)(參數(shù)表)刺彩、一個(gè)函數(shù)體(一系列語(yǔ)句)迷郑。
形式參數(shù)(parameter)的使用方式與局部變量非常類似,它們是由調(diào)用函數(shù)時(shí)的實(shí)際參數(shù)(argument)初始化的创倔。
調(diào)用函數(shù)時(shí)提供的實(shí)參數(shù)量可與形參數(shù)量不同嗡害,Lua會(huì)自動(dòng)調(diào)整實(shí)參數(shù)量,以匹配參數(shù)表的要求畦攘。這項(xiàng)調(diào)整與多重賦值(multiple assignment)很相似霸妹,即“若實(shí)參多于形參,則舍棄多于實(shí)參知押;若實(shí)參不足抑堡,則多余形參初始化為nil”。
function fn(a,b)
print(a,b);
end
fn(1) -- 1 nil
fn(1,2) -- 1 2
fn(1,2,3) -- 1 2
雖然這種調(diào)整行為會(huì)導(dǎo)致一些編程錯(cuò)誤朗徊,但它也是很有用的首妖,尤其是對(duì)于默認(rèn)實(shí)參的應(yīng)用。
function incCount(n)
n = n or 1 -- 函數(shù)以1作為默認(rèn)實(shí)參
count = count + n
print(n, count)
end
fn() -- attempt to perform arithmetic on a nil value (global 'count')
多重返回值
Lua函數(shù)具有一項(xiàng)非常與眾不同的特征爷恳,允許函數(shù)返回多個(gè)結(jié)果有缆。
Lua的幾個(gè)預(yù)定義函數(shù)就是返回多個(gè)值的,string.find()
用于在字符串中定位一個(gè)模式(pattern)温亲,該函數(shù)允許在字符串中找到指定的模式棚壁,將返回匹配的起始字符和結(jié)尾字符的索引。
startstr,endstr = string.find("hello lua", "lua")
print(startstr, endstr) -- 7 9
以Lua編寫(xiě)的函數(shù)同樣可以返回多個(gè)結(jié)果栈虚,只需在return
關(guān)鍵字后列出所有的返回值即可袖外。
例如:查找數(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會(huì)調(diào)整函數(shù)返回值數(shù)量以適應(yīng)不同的調(diào)用情況
- 若將函數(shù)調(diào)用作為一條單獨(dú)語(yǔ)句時(shí),Lua會(huì)丟棄函數(shù)的所有返回值魂务。
- 所將函數(shù)作為表達(dá)式的一部分來(lái)調(diào)用時(shí)曼验,Lua只保留函數(shù)的第一個(gè)返回值。只有當(dāng)一個(gè)函數(shù)調(diào)用是“一系列表達(dá)式”中最后一個(gè)元素或僅有一個(gè)元素時(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
這里所謂的“一系列表達(dá)式”,在Lua中表現(xiàn)為4種情況
- 多重賦值
- 函數(shù)調(diào)用時(shí)傳入的實(shí)參列表
-
table
的構(gòu)造式 -
return
語(yǔ)句
在多重賦值中孤紧,若函數(shù)調(diào)用是最后的或僅有的一個(gè)表達(dá)式豺裆,那么Lua會(huì)保留其盡可能多的返回值,用于匹配賦值變量号显。若函數(shù)沒(méi)有返回值或沒(méi)有足夠多的返回值臭猜,那么Lua會(huì)用nil
來(lái)補(bǔ)充缺失的值躺酒。若函數(shù)調(diào)用不是一些列表達(dá)式的最后一個(gè)元素,那么將只產(chǎn)生一個(gè)值蔑歌。
當(dāng)函數(shù)調(diào)用作為另一個(gè)函數(shù)調(diào)用的最后一個(gè)或僅有的實(shí)參時(shí)羹应,第一個(gè)函數(shù)所有返回值都將作為實(shí)參傳入第二個(gè)函數(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
構(gòu)造式可以完整地接收一個(gè)函數(shù)調(diào)用的所有結(jié)果丐膝,即不會(huì)有任何數(shù)量方面的調(diào)整。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
tbl1 = {fn1()} -- 相當(dāng)于 tbl1={}
tbl2 = {fn2()} -- 相當(dāng)于 tbl2={1}
tbl3 = {fn3()} -- 相當(dāng)于 tbl3={1,2}
不過(guò)這種行為只有當(dāng)一個(gè)函數(shù)調(diào)用作為最后一個(gè)元素時(shí)才會(huì)發(fā)生钾菊,而在其他位置上的函數(shù)調(diào)用總是只纏身給一個(gè)結(jié)果值帅矗。
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
語(yǔ)句,諸如return fn()
這樣的語(yǔ)句將返回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ù)調(diào)用放入一對(duì)圓括號(hào)中浑此,從而迫使它只返回一個(gè)結(jié)果。
print((fn(1)), (fn(2)), (fn(3))) -- nil 1 2
注意return
語(yǔ)句后面的內(nèi)容不需要圓括號(hào)滞详,在該位置上書(shū)寫(xiě)圓括號(hào)會(huì)導(dǎo)致不同的行為凛俱。
return (fn(3)) -- 只返回一個(gè)值,而無(wú)關(guān)于fn()返回幾個(gè)值
關(guān)于多重返回值還要介紹一個(gè)特殊函數(shù) unpack
料饥,unpack
接收一個(gè)數(shù)組作為參數(shù)蒲犬,并從下標(biāo)1開(kāi)始返回該數(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àng)重要用戶體現(xiàn)在“泛型調(diào)用(generic call)”機(jī)制中,泛型調(diào)用機(jī)制可動(dòng)態(tài)地以任何實(shí)參來(lái)調(diào)用任何函數(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
變長(zhǎng)參數(shù)
Lua函數(shù)可接受可變數(shù)量的參數(shù)悦荒,和C語(yǔ)言類似唯欣。在函數(shù)參數(shù)列表中使用...
表示函數(shù)有可變的參數(shù)。
Lua將函數(shù)的參數(shù)放在一個(gè)叫做arg
的表中搬味,除了參數(shù)外境氢,arg
表中還有一個(gè)域n
表示參數(shù)的個(gè)數(shù)。
function dump(...)
local str = ""
for i,v in ipairs(arg) do
str = str .. tostring(v).."\t"
end
return str.."\n"
end
例如:只想要string.find
返回的第二個(gè)值碰纬,典型的方法是使用虛變量_
产还。
local _, x = string.find(str, pattern)
Lua中的函數(shù)還可以接受不同數(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個(gè)點(diǎn)...
表示函數(shù)可接收不同數(shù)量的實(shí)參,當(dāng)函數(shù)被調(diào)用時(shí)嘀趟,它的所有參數(shù)都會(huì)被收集到一起脐区。這部分收集起來(lái)的實(shí)參稱為函數(shù)的“變長(zhǎng)參數(shù)(variable arguments)”。函數(shù)要訪問(wèn)它的變長(zhǎng)參數(shù)時(shí)她按,仍需要3個(gè)點(diǎn)牛隅。但不同的是炕柔,此時(shí)這個(gè)3個(gè)點(diǎn)是作為一個(gè)表達(dá)式來(lái)使用的。
function fn(...)
local x,y,z = ...
print(x,y,z)
end
fn(1,2) -- 1 2 nil
fn(1,2,3) -- 1 2 3
表達(dá)式...
的行為類似于一個(gè)具有多重返回值的函數(shù)媒佣,它返回的是當(dāng)前函數(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í),可使用...
參數(shù)收集到一個(gè)table
中默伍,但變參中函數(shù)非法的nil
時(shí)欢嘿,可使用select
函數(shù)將其過(guò)濾掉。
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
具名實(shí)參
Lua的函數(shù)參數(shù)是和位置相關(guān)的也糊,調(diào)用時(shí)會(huì)按順序依次傳遞給形式參數(shù)炼蹦。有時(shí)候,使用名字制定參數(shù)是很有用的(命名參數(shù))狸剃。
Lua中的參數(shù)傳遞機(jī)制和是具有“位置性”的掐隐,也就是說(shuō)在調(diào)用函數(shù)時(shí),實(shí)參時(shí)通過(guò)它在參數(shù)表中的位置與形參匹配起來(lái)钞馁。
function rename(tbl)
print(tbl, tbl.oriname, tbl.newname)
end
rename{oriname="ori.lua", newname="new.lua"}
Lua中特殊的函數(shù)調(diào)用語(yǔ)法虑省,當(dāng)實(shí)參只有一個(gè)table
構(gòu)造式時(shí),函數(shù)調(diào)用中的圓括號(hào)是可有可無(wú)的僧凰。
function rename(arg)
return os.rename(arg.old, arg.new)
end
第一類值
Lua中的函數(shù)是帶有詞法定界(lexical scoping)的第一類值(first-class values)探颈。
"第一類值"是指在Lua中函數(shù)和其他值(數(shù)值、字符串...)一樣训措,函數(shù)可以被存儲(chǔ)在變量中膝擂,可以存放在表中,可以作為函數(shù)的參數(shù)隙弛,也可以作為函數(shù)的返回值架馋。
“詞法界定”是指被嵌套的函數(shù)可以訪問(wèn)其他外部函數(shù)中的變量,這一特性給Lua提供了強(qiáng)大的編程能力全闷。
Lua中關(guān)于函數(shù)難以理解的是“函數(shù)是可以沒(méi)有名字的叉寂,也就是匿名的∽苤椋”屏鳍,但提到函數(shù)名時(shí),實(shí)際上是說(shuō)以一個(gè)指向函數(shù)的變量局服,和其他類型值得變量是一樣的钓瞭。
Lua中函數(shù)作為“第一類值”,表示函數(shù)可以存儲(chǔ)在變量中淫奔,可通過(guò)參數(shù)傳遞給其他函數(shù)山涡,還可作為其他函數(shù)的返回值。由于函數(shù)在Lua中是一種“第一類值”,所以不僅可將其存儲(chǔ)在全局變量中鸭丛,還可存儲(chǔ)在局部變量甚至table
的字段中竞穷。
Lua中函數(shù)是一種“第一類值(First-Class Value)”,它們具有特定的詞法域(Lexical Scoping)鳞溉。
“第一類值”是什么意思呢瘾带?這表示在Lua中函數(shù)與其他傳統(tǒng)類型的值具有相同的權(quán)利。函數(shù)可存儲(chǔ)到變量中或table
中熟菲,可作為實(shí)參傳遞給其他函數(shù)看政,還可作為其他函數(shù)的返回值。
“詞法域”是什么意思呢抄罕?這是指一個(gè)函數(shù)可以嵌套在另一個(gè)函數(shù)中允蚣,內(nèi)部的函數(shù)可訪問(wèn)外部函數(shù)的變量。這項(xiàng)聽(tīng)起來(lái)平凡的特性將給語(yǔ)言帶來(lái)極大的能力贞绵,因?yàn)樗试S在Lua中應(yīng)用各種函數(shù)式語(yǔ)言(functional-language)中強(qiáng)大的編程技術(shù)厉萝。
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對(duì)函數(shù)式編程(functional programming)提供了良好的支持
在Lua中有一個(gè)很容易混淆概念是恍飘,函數(shù)與所有其他值一樣都是匿名的榨崩,即它們都沒(méi)有名稱。當(dāng)討論一個(gè)函數(shù)名時(shí)章母,實(shí)際上是在討論一個(gè)持有某函數(shù)的變量母蛛。這與其他變量持有各種值的一個(gè)道理,可以以多種方式來(lái)操作這些變量乳怎。
如果說(shuō)函數(shù)是值的話彩郊,那是否可以說(shuō)函數(shù)就是由一些表達(dá)式創(chuàng)建的呢?是的蚪缀,事實(shí)上在Lua中最常見(jiàn)的是函數(shù)編寫(xiě)方式秫逝。
function fn(x)
return 2*x
end
-- 簡(jiǎn)化書(shū)寫(xiě)“語(yǔ)法糖”
fn = function(x) return 2*x end
一個(gè)函數(shù)定義實(shí)際就是一條語(yǔ)句,更準(zhǔn)確地說(shuō)是一條賦值語(yǔ)句询枚,這條語(yǔ)句創(chuàng)建了一種類型為“函數(shù)”的值违帆。
可將表達(dá)式function(x) <body> end
視為一種函數(shù)的構(gòu)造式,類似table
的構(gòu)造式{}
一樣金蜀。將這種函數(shù)構(gòu)造式的結(jié)果稱為一個(gè)“匿名函數(shù)”刷后,雖然一般情況下,會(huì)將函數(shù)賦予全局變量渊抄,即給予其一個(gè)名稱尝胆。但在某些特殊情況下,仍會(huì)需要用到匿名函數(shù)护桦。
Lua即可以調(diào)用以自身Lua語(yǔ)言編寫(xiě)的函數(shù)含衔,又可以調(diào)用以C語(yǔ)言編寫(xiě)的函數(shù)。
table庫(kù)
提供了一個(gè)函數(shù)table.sort
,它接收一個(gè)table
并對(duì)其中的元素排序抱慌。向這種函數(shù)就必須支持各種各樣可能的排序規(guī)則逊桦。sort
函數(shù)并沒(méi)有提供所有排序準(zhǔn)則,而是提供了一個(gè)可選的參數(shù)抑进,所謂“次序函數(shù)(order function)”强经。這個(gè)函數(shù)接收兩個(gè)元素,并返回在有序情況下第一個(gè)元素是否已排在第二個(gè)元素之前寺渗。
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ù)匿情,接收另一個(gè)函數(shù)作為實(shí)參,稱其為“高階函數(shù)(higher-order function)”信殊。高階函數(shù)是一種強(qiáng)大的編程機(jī)制炬称,應(yīng)用匿名函數(shù)來(lái)創(chuàng)建高階函數(shù)所需的實(shí)參則可以帶來(lái)更大的靈活性。但請(qǐng)記住涡拘,高階函數(shù)并沒(méi)有什么特權(quán)玲躯。Lua強(qiáng)調(diào)將函數(shù)視為“第一類值”,所以高階函數(shù)只是一種基于該觀點(diǎn)的應(yīng)用體現(xiàn)而已鳄乏。
例如:關(guān)于導(dǎo)數(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ù)
當(dāng)一個(gè)函數(shù)內(nèi)部嵌套另一個(gè)函數(shù)定義時(shí)跷车,內(nèi)部的函數(shù)體可以訪問(wèn)外部函數(shù)的局部變量,這種特征稱之為“詞法界定”橱野。詞法界定加上第一類函數(shù)在編程語(yǔ)言中是一個(gè)功能強(qiáng)大的工具朽缴。
若將函數(shù)寫(xiě)在另一個(gè)函數(shù)何內(nèi),那么這個(gè)位于內(nèi)部的函數(shù)便可訪問(wèn)外部函數(shù)中的局部變量水援,這項(xiàng)特征稱之為“詞法域”密强。
例如:根據(jù)每個(gè)學(xué)生的年級(jí)來(lái)對(duì)它們姓名進(jìn)行由高到低的排序
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ù)可以訪問(wèn)參數(shù)grades
蜗元,而grades
是外部函數(shù)sort_by_grade
的局部變量或渤。在這個(gè)匿名函數(shù)內(nèi)部,grades
即不是全局變量也不是局部變量奕扣,將其稱為一個(gè)非局部的變量(non-local variable)或upvalues
或“外部的局部變量(external local variable)”薪鹦。為什么在Lua中允許這種訪問(wèn)呢?原因在于函數(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ù)訪問(wèn)了一個(gè)“非局部的變量i
距芬,i
變量用于保持一個(gè)計(jì)數(shù)器。Lua會(huì)以closure
的概念來(lái)正確地處理這種情況循帐。簡(jiǎn)單來(lái)說(shuō)框仔,一個(gè)closure
就是一個(gè)函數(shù)加上該函數(shù)所需訪問(wèn)的所有“非局部的變量”。如果再次調(diào)用count()
拄养,那么它會(huì)創(chuàng)建一個(gè)新的局部變量i
离斩,從而也將得到一個(gè)新的closure
银舱。
技術(shù)上來(lái)講,閉包指的是值而不是函數(shù)跛梗,函數(shù)僅僅是閉包的原型聲明寻馏,盡管如此,在不會(huì)導(dǎo)致混淆的情況下核偿,使用術(shù)語(yǔ)函數(shù)代指閉包诚欠。
從技術(shù)上講,Lua中只有closure
漾岳,而不存在函數(shù)轰绵。因此,函數(shù)本身就是一種特殊的closure
尼荆。不過(guò)只要不會(huì)引起混淆左腔,仍將采用術(shù)語(yǔ)函數(shù)來(lái)指代closure
。
在許多場(chǎng)合中closure
都是一種很有價(jià)值的工具捅儒,例如:可作為sort
類高階函數(shù)的參數(shù)液样。closure
對(duì)于創(chuàng)建其他函數(shù)也很有價(jià)值。這種機(jī)制使Lua可混合在函數(shù)式編程世界中久經(jīng)考驗(yàn)的編程技術(shù)巧还。另外closure
對(duì)于回調(diào)函數(shù)也很有用鞭莽。
例如,假設(shè)有一個(gè)傳統(tǒng)的GUI工具包可以創(chuàng)建按鈕狞悲,每個(gè)按鈕都有一個(gè)回調(diào)函數(shù)撮抓,每當(dāng)用戶按下按鈕是GUI工具包都會(huì)調(diào)用這些回調(diào)函數(shù)妇斤。再假設(shè)摇锋,基于此要做一個(gè)十進(jìn)制計(jì)算器,其中需要10個(gè)數(shù)字按鈕站超,你會(huì)發(fā)現(xiàn)這些按鈕之間的區(qū)別其實(shí)并不大荸恕,僅需在按下不同按鈕時(shí)稍微不同的操作就可以了。
-- 創(chuàng)建按鈕
function digitButton(digit)
return Button{label=tostring(digit), action=function() add_to_display(digit) end}
end
假設(shè)Button
是工具包中一個(gè)用于創(chuàng)建新按鈕的函數(shù)死相,label是按鈕的標(biāo)簽融求,action
是回調(diào)closure
,每當(dāng)按鈕按下時(shí)就會(huì)調(diào)用它算撮∩穑回調(diào)一般發(fā)生在digitButton
函數(shù)執(zhí)行完后,那時(shí)局部變量digit
已經(jīng)超出了作用范圍肮柜,但closure
仍可以訪問(wèn)到這個(gè)變量陷舅。
closure
在另一種情況中也非常有用,例如在Lua中函數(shù)是存儲(chǔ)在普通變量中的审洞,因此可以輕易地重新定義某些函數(shù)莱睁,甚至是重新定義那些預(yù)定義的函數(shù)。這也是Lua相當(dāng)靈活的原因之一。
通常當(dāng)重新定義一個(gè)函數(shù)時(shí)仰剿,需要在新的實(shí)現(xiàn)中調(diào)用原來(lái)的那個(gè)函數(shù)创淡。舉例來(lái)說(shuō),假設(shè)要重新定義函數(shù)sin南吮,使其參數(shù)能使用角度來(lái)替代原先的弧度琳彩。那么這個(gè)新函數(shù)就必須得轉(zhuǎn)換它的實(shí)參,并調(diào)用原來(lái)的sin函數(shù)完成真正的計(jì)算部凑。
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
保存到一個(gè)私有變量汁针,現(xiàn)在只有通過(guò)新版本的sin才能訪問(wèn)到它。
可以使用同樣的技術(shù)來(lái)創(chuàng)建一個(gè)安全的運(yùn)行環(huán)境砚尽,即所謂的沙盒(sandbox)施无。當(dāng)執(zhí)行一些未受信任的代碼時(shí)就需要一個(gè)安全的運(yùn)行環(huán)境,例如在服務(wù)器中執(zhí)行那些從Internet上接收到的代碼必孤。
舉例來(lái)說(shuō)猾骡,如果要限制一個(gè)程序訪問(wèn)文件的話,只需要使用closure來(lái)重新定義函數(shù)io.open()
即可敷搪。
do
local oldOpen = io.open
local accessOK = function(filename,mode)
-- 檢查訪問(wèn)權(quán)限
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)過(guò)重新定義后兴想,一個(gè)函數(shù)就只能通過(guò)受限版本來(lái)調(diào)用原來(lái)那個(gè)未受限的open()
函數(shù)了。將原來(lái)不安全的版本保存到closure的一個(gè)私有變量中赡勘,從而使得外部再也無(wú)法直接訪問(wèn)到原來(lái)的版本了嫂便。
通過(guò)這種技術(shù),可以再Lua的語(yǔ)言層面上就構(gòu)建出一個(gè)安全的運(yùn)行環(huán)境闸与,且不失建議性和靈活性毙替。相對(duì)于提供一套大而全的解決方案,Lua提供的則是一套“元機(jī)制(meta-mechanism)”践樱,因此可以根據(jù)特定的安全需要來(lái)創(chuàng)建一個(gè)安全的運(yùn)行環(huán)境厂画。
非全局的函數(shù)
由于函數(shù)是一種“第一類值”,因此一個(gè)顯而易見(jiàn)的推論是拷邢,函數(shù)不僅可以存儲(chǔ)在全局變量中袱院,也可以存儲(chǔ)在table的字段中和局部變量中。
Lua中大部分庫(kù)采用將函數(shù)存儲(chǔ)在table
字段中這種機(jī)制瞭稼。若要在Lua中創(chuàng)建這種函數(shù)忽洛,只需將常規(guī)的函數(shù)語(yǔ)法和table
語(yǔ)法結(jié)合即可。
Lib = {}
Lib.foo = function(x,y)
return x+y
end
Lib.bar = function(x,y)
return x-y
end
當(dāng)然环肘,也可使用構(gòu)造式:
Lib = {
foo=function(x,y) return x+y end,
bar=function(x,y) return x-y end
}
只要將一個(gè)函數(shù)存儲(chǔ)到一個(gè)局部變量中欲虚,即得到一個(gè)局部函數(shù)(local function),也就是說(shuō)該函數(shù)只能在某個(gè)特定的作用域中使用廷臼。
對(duì)于程序包(package)而言苍在,這種函數(shù)定義是非常有用的绝页,因?yàn)長(zhǎng)ua是將每個(gè)特定程序塊(chunk)作為一個(gè)函數(shù)來(lái)處理的,所以在一個(gè)程序塊中聲明的函數(shù)就是局部函數(shù)寂恬,這些局部函數(shù)只在該程序塊中可見(jiàn)续誉。詞法域確保了程序包中的其他函數(shù)可以使用這些局部函數(shù)。
local fn = function(arg)
-- function body
end
local func = function(arg)
fn()
end
對(duì)于局部函數(shù)的定義初肉,Lua支持一種特殊的語(yǔ)法糖:
local function fn(arg)
-- function body
end
在定義遞歸的局部函數(shù)中酷鸦,有一個(gè)特別之處需要注意罐孝。
local face = function(n)
if n==0 then
return 1
else
-- 錯(cuò)誤的遞歸調(diào)用
-- 當(dāng)Lua編譯到函數(shù)體中調(diào)用fact(n-1)的地方時(shí)机隙,由于局部的fact尚未定義完畢。
-- 因此這句表達(dá)式其實(shí)調(diào)用了一個(gè)全局的fact盲泛,而非此函數(shù)本身妄壶。
return n*face(n-1)
end
end
--[[結(jié)局方案:可以先定義一個(gè)局部變量摔握,然后再定義函數(shù)本身。--]]
local fact
fact = function(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
--[[
現(xiàn)在函數(shù)中的fact調(diào)用就表示了局部變量丁寄,即使在函數(shù)定義時(shí)氨淌,這個(gè)局部變量的值尚未完成定義,但之后在函數(shù)執(zhí)行時(shí)伊磺,fact則肯定擁有了正確的值盛正。
--]]
當(dāng)Lua展開(kāi)局部函數(shù)定義的“語(yǔ)法糖”時(shí),并不是使用基礎(chǔ)函數(shù)定義語(yǔ)法屑埋。而是對(duì)于局部函數(shù)定義:
local function fn(<args>) <function body> end
Lua將其展開(kāi)為
local fn
fn = function(<args>) <function body> end
因此豪筝,使用此種語(yǔ)法定義遞歸函數(shù)不會(huì)產(chǎn)生錯(cuò)誤:
local function fact(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
當(dāng)然,這個(gè)技巧對(duì)于間接遞歸的函數(shù)而言是無(wú)效的摘能。對(duì)于間接遞歸的情況下续崖,必須使用一個(gè)明確的向前聲明(Forward Declaration):
local fn,func -- 向前聲明
function func()
fn()
end
function fn()
func()
end
--[[
注意,別把第二個(gè)函數(shù)定義為"local function fn"徊哑。
如果那樣的話袜刷,Lua會(huì)創(chuàng)建一個(gè)全新的局部變量fn聪富。
而將原來(lái)聲明的fn(func函數(shù)中所引用的那個(gè))置于未定義狀態(tài)莺丑。
--]]
尾調(diào)用
Lua函數(shù)有一個(gè)有趣的特征,那就是Lua支持“尾調(diào)用消除(tail-call elimination)”墩蔓。
所謂“尾調(diào)用(tail call)”就是一種類似于goto()
的函數(shù)調(diào)用梢莽,當(dāng)一個(gè)函數(shù)調(diào)用是另一個(gè)函數(shù)的最后一個(gè)動(dòng)作時(shí),該調(diào)用才算是一條“尾調(diào)用”奸披。
function fn(x)
return func(x)
end
--[[
當(dāng)fn()函數(shù)調(diào)用完func()函數(shù)之后就再無(wú)其他事情可做了
因此在這種情況下昏名,程序就不需要返回那個(gè)“尾調(diào)用”所在的函數(shù)了。
所以在“尾調(diào)用”之后阵面,程序也不需要保存任何關(guān)于該函數(shù)的棧(stack)信息轻局。
當(dāng)func()函數(shù)返回時(shí)洪鸭,執(zhí)行控制權(quán)可以直接返回給調(diào)用fn()函數(shù)的那個(gè)點(diǎn)上。
有些語(yǔ)言實(shí)現(xiàn)(例如Lua解釋器)可以得益于這個(gè)特點(diǎn)仑扑,使得在進(jìn)行尾調(diào)用時(shí)不耗費(fèi)任何椑谰簦空間。
將這種實(shí)現(xiàn)稱之為支持“尾調(diào)用消除”镇饮。
--]]
由于“尾調(diào)用”不會(huì)耗費(fèi)楎阎瘢空間,所以一個(gè)程序可以擁有無(wú)數(shù)嵌套的“尾調(diào)用”储藐。
function fn(n)
if n>0 then
return fn(n-1)
end
end
--[[
在調(diào)用fn()函數(shù)時(shí)俱济,傳入任何數(shù)字n作為參數(shù)都不會(huì)造成棧溢出。
--]]
有一點(diǎn)需要注意的是钙勃,當(dāng)想要受益于“尾調(diào)用消除”時(shí)蛛碌,務(wù)必要確定當(dāng)前的調(diào)用時(shí)一條“尾調(diào)用”。判斷的準(zhǔn)則是“一個(gè)函數(shù)在調(diào)用完另一個(gè)函數(shù)之后辖源,是否就無(wú)其他事情需要做了”左医。有些看似“尾調(diào)用”的代碼,其實(shí)都違背了這條準(zhǔn)則同木。
function fn(x)
func(x)
end
--[[
當(dāng)調(diào)用完func()函數(shù)后浮梢,fn()函數(shù)并不能立即返回,它還需丟棄func()函數(shù)返回的臨時(shí)結(jié)果彤路。
--]]
Lua中秕硝,只有return <func>(<args>)
這樣的調(diào)用形式才算是一條“尾調(diào)用”。
return fn(x)+1 -- 必須做一次加法
return x or fn(x) -- 必須調(diào)整為一個(gè)返回值
return (g(x)) -- 必須調(diào)整為一個(gè)返回值
Lua在調(diào)用前對(duì)<func>
及其參數(shù)求值洲尊,所以它們可以是任意復(fù)雜的表達(dá)式远豺。
return x[i].fn(x[j]+a*b, i+j)
一條“尾調(diào)用”就好比是一條goto
語(yǔ)句。因此坞嘀,在Lua中“尾調(diào)用”的一大應(yīng)用就是編寫(xiě)“狀態(tài)機(jī)(state machine)”躯护。這種程序通常以一個(gè)函數(shù)來(lái)表示一個(gè)狀態(tài),改變狀態(tài)就是goto
(或調(diào)用)到另一個(gè)特定的函數(shù)和丽涩。
例如:一個(gè)簡(jiǎn)單的迷宮游戲中棺滞,一個(gè)迷宮有幾間房間,每間房中最多有東南西北4扇門(mén)矢渊。用戶在每一步異動(dòng)中都需要輸入一個(gè)移動(dòng)的方向继准。如果在某個(gè)方向上有門(mén),那么用戶可以進(jìn)入相應(yīng)的房間矮男。不然移必,程序就打印一條警告。游戲的目標(biāo)就是讓用戶從最初的房間走到最終的房間毡鉴。
這個(gè)游戲就是典型的狀態(tài)機(jī)崔泵,其中當(dāng)前房間就是一個(gè)狀態(tài)秒赤。可以將迷宮的每間房實(shí)現(xiàn)為一個(gè)函數(shù)憎瘸,并使用“尾調(diào)用”來(lái)實(shí)現(xiàn)從一間房移動(dòng)到另一件倒脓。
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
-- 調(diào)用初始房間來(lái)開(kāi)始游戲
room1()
--[[
若沒(méi)有“尾調(diào)用消除”,每次用戶的異動(dòng)都會(huì)創(chuàng)建一個(gè)新的棧層(stack level)
異動(dòng)若干步后就有可能會(huì)導(dǎo)致棧溢出
“尾調(diào)用消除”則對(duì)用戶異動(dòng)的次數(shù)沒(méi)有任何限制
因?yàn)槊看萎悇?dòng)實(shí)際上都只是完成一條goto語(yǔ)句到另一個(gè)函數(shù)含思,而非傳統(tǒng)的函數(shù)調(diào)用崎弃。
--]]
對(duì)于簡(jiǎn)單的游戲而言,或許覺(jué)得將程序設(shè)計(jì)為數(shù)據(jù)驅(qū)動(dòng)的會(huì)更好一些含潘,其中將房間和異動(dòng)記錄在table中饲做。不過(guò),如果游戲中每間房間都有各自特殊的情況的話遏弱,采用這種狀態(tài)機(jī)的設(shè)計(jì)則更為合適盆均。
Lua迭代器與閉包
迭代器是一種可以遍歷(iterate over)集合中所有元素的機(jī)制。在Lua中通常將迭代器表示為函數(shù)漱逸,每次調(diào)用函數(shù)即返回集合中的“下一個(gè)”元素泪姨。
每個(gè)迭代器都需要在每次成功調(diào)用之間保持一些狀態(tài),這樣才能知道它所在的位置及如何步進(jìn)到下一個(gè)位置饰抒。閉包(closure)對(duì)于這類任務(wù)提供極佳的支持肮砾,一個(gè)閉包就是一種可以訪問(wèn)其外部嵌套環(huán)境中的局部變量的函數(shù)。
對(duì)于閉包而言袋坑,這些變量就可用于在成功調(diào)用之間保持狀態(tài)值仗处,從而使閉包可以記住它在一次遍歷中所在的位置。
當(dāng)然枣宫,為了創(chuàng)建一個(gè)新的閉包婆誓,還必須創(chuàng)建它的“非局部變量(non-local variable)”。因此也颤,一個(gè)閉包結(jié)構(gòu)通常涉及到兩個(gè)函數(shù):閉包本身和一個(gè)用于創(chuàng)建該閉包的工廠函數(shù)洋幻。
-- 為列表編寫(xiě)簡(jiǎn)單的迭代器,與ipaires不同的是該迭代器并不是返回每個(gè)元素的索引翅娶,而是返回元素的值文留。
-- values是一個(gè)工廠,每當(dāng)調(diào)用這個(gè)工廠時(shí)故觅,就創(chuàng)建一個(gè)新的閉包(迭代器本身)厂庇。這個(gè)閉包將它的狀態(tài)保存在其外部變量tbl和i中。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 每當(dāng)調(diào)用這個(gè)迭代器時(shí)输吏,它就從列表tbl中返回下一個(gè)值。
-- 直到最后一個(gè)元素返回后替蛉,迭代器就會(huì)返回nil贯溅,以此表示迭代器的結(jié)束拄氯。
tbl={1,2,3,4}
iter = values(tbl) -- 創(chuàng)建迭代器
while true do
local el = iter() -- 調(diào)用迭代器
if el==nil then
break
end
print(el)
end
然而,使用泛型for
則更為簡(jiǎn)單它浅,你會(huì)發(fā)現(xiàn)泛型for正是為這種迭代而設(shè)計(jì)的译柏。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 泛型for為一次迭代循環(huán)做了所有的薄記工作。
-- 它在內(nèi)部保存了迭代器函數(shù)姐霍,因此不再需要iter變量鄙麦。
-- 它在每次新迭代時(shí)調(diào)用迭代器,并在迭代器返回nil時(shí)結(jié)束循環(huán)镊折。
tbl={1,2,3,4}
for el in values(tbl) do
print(el)
end
需求:遍歷當(dāng)前輸入文件中所有單詞的迭代器
為完成這樣的遍歷胯府,需要保持兩個(gè)值:當(dāng)前行的內(nèi)容、當(dāng)前行所處的位置恨胚。有了這些信息就可以不斷產(chǎn)生下一個(gè)單詞骂因。迭代器函數(shù)的主要部分使用 string.find
在當(dāng)前行中以當(dāng)前位置作為起始來(lái)所搜索一個(gè)單詞。使用模式%w+
來(lái)描述一個(gè)單詞赃泡,它用于匹配一個(gè)或多個(gè)的文字/數(shù)字字符寒波。如果string.find
找到了一個(gè)單詞,迭代器就會(huì)將當(dāng)前位置更新為該單詞之后的第一個(gè)字符升熊,并返回該單詞俄烁。否認(rèn)則,它就讀取新的一行并反復(fù)這個(gè)搜索過(guò)程级野。若沒(méi)有剩余的行猴娩,則返回nil
表示迭代的結(jié)束。
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