在Lua中,你可以像使用number和string一樣使用function。可以將function存儲到變量中,存儲到table中,可以當作函數參數傳遞,可以作為函數的返回值。
在Lua中,function跟其他值一樣,也是匿名的。function被作為一個值存儲在變量中,下面這個例子有點2,可以幫助理解:
~~~
a = {p = print}
a.p("Hello World") --> Hello World
print = math.sin -- 'print' now refers to the sin function
a.p(print(1)) --> 0.841470
sin = a.p -- 'sin' now refers to the print function
sin(10, 20) --> 10 20
~~~
創建函數的表達式
~~~
function foo (x) return 2*x end
--實際上為:
foo = function (x) return 2*x end
~~~
從上面可以看出,function的定義實際上是創建一個'function'類型的值,并賦值給一個變量。我們可以將*function (x) body end?*看成是function的構造函數,就像{}是table的構造函數一樣。
table庫有一個函數*table.sort?*,接受一個table,然后對table中的元素排序。排序可能有各種規則,升序,降序,數字或字母表順序,根據table中的key排序等。該函數沒有提供各種各樣所有的排序選項,而是提供了一個單獨的選項,即函數的第二個參數,*order*函數,*order*函數,接受兩個元素,返回一個布爾值,來指示是否第一個元素排在第二個元素之前。看下面的例子:
~~~
network = {
{name = "grauna", IP = "210.26.30.34"},
{name = "arraial", IP = "210.26.30.23"},
{name = "lua", IP = "210.26.23.12"},
{name = "derain", IP = "210.26.23.20"},
}
table.sort(network, function (a,b) return (a.name > b.name) end)
~~~
從上面看,匿名函數使用起來很方便吧。
這里先講一個概念,higher-order function。它可以把其他函數當作參數。下面給個示例,求導函數(哎,好多年前的東西,一點都不記得了,還跟同事討論了半天)
~~~
function derivative (f, delta)
delta = delta or 1e-4
return function (x)
return (f(x + delta) - f(x))/delta
end
end
c = derivative(math.sin)
print(math.cos(10), c(10))
--> -0.83907152907645 -0.83904432662041
~~~
文章開頭也說了,Lua中的function可以像普通的值一樣使用,可以存儲到全局變量,局部變量,table中。往下看,function存儲到table中,這是個牛B的特性,可以實現很多高級的功能,例如模塊,面向對象等。
## 1. 閉合函數
先看一個示例,有兩個table,一個table中是student names,另一個table里面有每個student的grade;現在要根據student的grade來對前一個table進行排序。根據之前所學,可以使用如下code:
~~~
names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2] -- compare the grades
end)
~~~
現在寫一個function來實現這一個功能:
~~~
function sortbygrade (names, grades)
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2] -- compare the grades
end)
end
~~~
上面這段code的有趣之處在于,sort中的匿名函數可以訪問參數中的grades,grades是sortbygrade的局部變量。在該匿名函數中,grades既不是局部變量,也不是全局變量,而是非局部變量(智商有點捉雞了)。
現在來看看這個非局部變量的妙用。看下面示例:
~~~
function newCounter ()
local i = 0
return function () -- anonymous function
i = i + 1
return i
end
end
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
~~~
在上面的代碼中,匿名函數引用了非局部變量*i?*,來記數。但是,調用這個匿名函數的時候,i已經不再在有效作用域了,因為創建*i?*的函數(*newCounter?*)已經返回了。但是Lua可以使用閉合函數正確處理這種情況。簡單地說,閉合函數就是一個函數加上要訪問一個非局部變量所需要的所有元素。如果再調用一個*newCounter?*,它會重新創建一個新的非局部變量i, 得到一個新的閉合函數。如上面示例,c1和c2是兩個基于用一個函數的閉合函數,二者有兩個獨立的非局部變量*i?*的實例。
閉合函數在很多場合都很好用。例如,在higer-order函數(如*sort?*)中,可以作為參數;在newCounter這樣的在自己函數體中創建其他的函數;作為回調函數。一個典型的例子就是,在傳統的GUI工具箱中,創建按鈕,每個按鈕需要一個回調函數來響應按鍵動作。例如一個計算器,需要10個類似的按鈕,每個數字一個。可以用下面的function來創建:
~~~
function digitButton (digit)
return Button{ label = tostring(digit),
action = function ()
add_to_display(digit)
end
}
end
~~~
在上面的示例中,我們假設digitButton是一個工具箱函數,用來創建一個按鈕,lable是按鈕的label, action是回調函數,響應按鍵操作。這個回調函數可以在digitButton完成工作以后很久,局部變量digit超出作用域后再調用,但是它仍然能夠訪問變量digit。
由于函數是存儲在普通的變量中,Lua可以方便的重新定義一些函數,甚至Lua預定義的函數。下面看一個示例,重新定義sin函數,將參數由原來的弧度,改成度數。新的函數一定要轉換一下參數的值,然后調用原來的sin函數來實現功能。
~~~
oldSin = math.sin
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
~~~
推薦使用第二種實現,我們將原來的函數保存在一個局部變量中中,訪問它的唯一途徑就是通過新版本的函數。通過這種技巧,可以構建沙箱安全環境。這種安全環境在你需要運行一些不被信任的代碼(例如從internet上收到的代碼)時是很有必要的。例如,為了嚴格限制程序可以訪問的文件,可以重新定義函數io.open:
~~~
do
local oldOpen = io.open
local access_OK = function (filename, mode)
--here to add some code to restrict access
<check access>
end
io.open = function (filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end
~~~
如下代碼,重定義open函數后,程序沒辦法訪問無限制版本的open函數,只能使用這個限制版本。通過這種方法,Lua可以簡便靈活地構建安全的沙箱環境。
## 2.非全局函數
Lua中的函數可以存儲到全局變量中,也可以存儲到table和局部變量中。
大多數的Lua庫都是將函數存入table中。下面示例集中定義table中函數的方法:
~~~
Lib = {}
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end
--使用構造函數
Lib = {
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
}
--另一種語法
Lib = {}
function Lib.foo (x,y) return x + y end
function Lib.goo (x,y) return x - y end
~~~
當我們將函數存儲到一個局部變量中,我們就得到一個局部函數,只在給定的作用域中有效。這個特性在包中經常用到,可以在一個包中定義局部函數,這些函數只在該包中可見,包中的其他函數可以調用這些局部函數:
~~~
local f = function (<params>)
<body>
end
local g = function (<params>)
<some code>
f() -- 'f' is visible here
<some code>
end
~~~
在**遞歸**函數中有一點微妙,看下面兩份代碼。
~~~
local fact = function (n)
if n == 0 then return 1
else return n*fact(n-1) -- buggy
end
end
--下面這個可以工作
local fact
fact = function (n)
if n == 0 then return 1
else return n*fact(n-1)
end
end
~~~
再看下面這個局部函數定義展開后的形式:
~~~
local function foo (<params>) <body> end
--expands to
local foo
foo = function (<params>) <body> end
~~~
因此,上面的遞歸函數也可以寫成,注意3中實現方式的不同。
~~~
local function fact (n)
if n == 0 then return 1
else return n*fact(n-1)
end
end
~~~
但是,上面這個技巧在非直接遞歸函數中就不好使了啊。看下面的示例:
~~~
local f, g -- 'forward' declarations
function g ()
<some code>
f()
<some code>
end
--local f 如果這句放在這,那么上面的g()是引用不到正確的f函數的
function f ()
<some code>
g()
<some code>
end
--調用一下,要把上面的代碼補全哦
g()
~~~
不信,試試把local f這句放到fucntion g ()的定義后面看有什么后果。
## 3.強大的尾調用
程序員都知道,函數的調用會產生調用堆棧。但是在Lua中,當然也有調用堆棧啦。但是尾調用在Lua中就不同于其他編程語言啦。我們先通過幾行代碼來看下什么是尾調用tailor call
~~~
function f (x) return g(x) end
function foo (n)
if n > 0 then
return foo(n - 1)
end
end
--下面這幾個都不是
function f (x)
g(x) --after calling g, f still has to discard occasional results from g before returning
end
function f (x)
return g(x) + 1 -- must do the addition
end
function f (x)
return x or g(x) -- must adjust to 1 result
end
function f (x)
return (g(x)) -- must adjust to 1 result
end
~~~
在Lua中,只用格式為return func(args)的調用才是尾調用。即使func和args都是復雜的表達式也沒關系,因為Lua在調用之前算得到它們的值。所以下面這個也是尾調用
~~~
return x[i].foo(x[j] + a*b, i + j)
~~~
我們再來針對下面這行代碼講講Lua中對尾調用處理的強大。
~~~
function f (x) return g(x) end
~~~
Lua是怎么處理尾調用的呢。像C語言的話,上面代碼中,f對g調用后,g執行完畢后,會返回到g的被調用處。但是在Lua中,這是一個尾調用,g執行完畢之后,會直接返回到f的被調用處。這樣的話,可以節省很多堆棧空間。因此像下面這個函數,就不需擔心n太大的話會有溢出。
在Lua中,對尾調用的一個很好的應用是狀態機。可以用一個函數表示一個狀態,改變狀態就是跳轉到一個指定的函數。下面我們用一個簡單的迷宮程序示例:迷宮有幾個房間(我們這里是4個),每個房間有4扇門,通向東,南,西,北。每一步,玩家指定一個移動的方向,如果這個方向有門,那么就進入對于的房間;否則,程序給一個警告;目標是從開始的房間走到目標房間。
這個程序是一個典型的狀態機,狀態就是當前的房間,每個房間寫一個函數。用尾調用來從一個房間移動到另一個房間。如果不用尾調用的話,每一次移動都要將堆棧升級一個level,一定數量的移動后,可能就會導致程序溢出了。使用尾調用的話,就不需要擔心這個問題了。廢話少說,下面是代碼,已經驗證過了,比較簡單。
~~~
function room1 ()
local move = io.read()
if move == "south" then return room3()
elseif move == "east" then return room2()
else
print("invalid move")
return room1() -- stay in the same room
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 == "east" then return room4()
else
print("invalid move")
return room3()
end
end
function room4 ()
print("congratulations!")
end
--寫完上面的四個room,調用一下就可以了。
room1()
~~~
水平有限,如果有朋友發現錯誤,歡迎留言交流。