Lua函數(shù)

函數(shù)有兩種用途:

  1. 完成指定任務,此時函數(shù)作為調用語句使用。
  2. 計算并返回值,此時函數(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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,246評論 0 38
  • 雖然計算機可以做很多事情,但它不會思考,它需要接受系統(tǒng)化的指令來工作。大部分用戶通過應用程序為計算機指派任務,軟件...
    JunChow520閱讀 7,264評論 0 4
  • 《超級個體-伽藍214》85/100,1.31日打卡,天氣多云 【三件事】 1. 聽每日得到音頻 2. 閱讀+筆記...
    伽藍214閱讀 127評論 0 0
  • 2018年04月26日 星期四 親子日記第110天 今天孩子進行了期中考試,于是下午我早早回到家,給孩子做...
    夢_0ba6閱讀 273評論 0 1
  • 23歲之前從未到過我現(xiàn)在所在的城市,畢業(yè)之前“奉命”來到佛山,格外陌生…… 天下著大雨,我從學校出...
    D034雨愛雨_佛山閱讀 109評論 1 6

友情鏈接更多精彩內容