函式教學

lua-users home
wiki

函式允許你將程式碼存到一個值,可讓你在多個地方執行相同的程式碼,而不必重複它。另外,它們允許你在執行時變更程式行為,只要將不同的函式提供給程式碼的不同部分即可。

定義函式

函式以 function 關鍵字建立,如下所示:

function ( args ) body end

下例顯示一個簡單的函式,接收單一參數並回傳其兩倍的值

> foo = function (n) return n*2 end
> = foo(7)
14

參數 (也稱為 變數) 指定在 ( ) 部分中,而值會使用 return 關鍵字從函式中回傳。沒有 return,函式將不會回傳任何值。


函式是值

請注意,在以上範例中,我們並沒有真正「命名」這個函式,我們僅將其指定給一個變數。這是因為 Lua 中的函式是規律性值 (例數字、字串、表格等),而且你可以使用任何可以用來操作其他值的動作來操作它們。這與你可能了解的許多其他語言 (例如 C) 有很大的不同,其中函式在編譯時具有固定的永久名稱,而且不能像值一樣被操作。

function 區塊是一個表達式 (就和「1 + 2」是一個表達式一樣),會評估為一個新的函式值。函式值可以使用 ( ) 函式執行函式中的程式碼。( ) 對會在函式表達式後面,可以包含以逗號分隔的參數清單 (選擇性)。

這代表 Lua 函式被認為是 匿名的 (沒有預設名稱) 和 一級的 (不受不同於其他值的對待)。

還要記得的一件事是,函式與表格類似,是透過參考傳遞。例如,當你指定包含函式的變數到其他變數時,你只是為相同的函式建立一個新的「處理程式」。


函式參數

函式可以接受 0 或更多參數。這些是呼叫函式時提供給函式的值,而函式中儲存的程式碼可以使用這些值。在函式中,變數看起來就像變數,只不過它們只存在於函式中。

展示參數如何運作以及如何將它們傳遞至函式的範例

> f = function (op, a, b)
>>  if op == 'add' then
>>   return a + b
>>  elseif op == 'sub' then
>>   return a - b
>>  end
>>  error("invalid operation")
>> end
> g = function (value)
>>  print(value)
>> end
> = f('add', 1, 2) -- args are given inside (), separated by commas.
3
> = f('add', 1, 2, 123) -- extra args are ignored
3
> = f('add', 1) -- missing args aren't an error, instead they will be filled with nil, which might cause an error in the function's code
stdin:1: attempt to perform arithmetic on local 'b' (a nil value)
> = g() -- to call a function with no args, use ()
nil
> = g "example" -- the () can be omitted if you have one quoted string arg
example
> = g {} -- same with one table constructor
table: 0x820ee0

函式回傳值

函式也可以使用 return 關鍵字將值回傳給呼叫它們的程式碼。那個值會成為函式呼叫表達式的值。Lua 的獨特功能是函式可以回傳任意數量的值。在大部分的語言中,函式總是回傳一個值。若要使用這項功能,請在 return 關鍵字後放置以逗號分隔的值

> f = function ()
>>  return "x", "y", "z" -- return 3 values
>> end
> a, b, c, d = f() -- assign the 3 values to 4 variables. the 4th variable will be filled with nil
> = a, b, c, d
x y z nil
> a, b = (f()) -- wrapping a function call in () discards multiple return values
> = a, b
x, nil
> = "w"..f() -- using a function call as a sub-expression discards multiple returns
wx
> print(f(), "w") -- same when used as the arg for another function call...
x w
> print("w", f()) -- ...except when it's the last arg
w x y z
> print("w", (f())) -- wrapping in () also works here like it does with =
w x
> t = {f()} -- multiple returns can be stored in a table
> = t[1], t[2], t[3]
x y z

一件要記住的最後範例 ( {f()} ) 是如果函數傳回 nil,由於表單中的 nil 被認為是「沒值」,# 算子不能可靠地用於取得值的數量,因為如果一個陣列有「洞」會是未定義的。

如果你習慣了使用(像 Python)語言,它會透過儲存在「元組」類型來傳回多個值,這不是 Lua 的運作方式。Lua 函數實際上傳回個別值,而不是單一容器。


Return 會略過其他程式碼

function f(switch)

	if not switch then --if switch is nil, function f() will not complete anything else below Return
		return
	end

	print("Hello") 
end

f()--doesn't print anything

function f(switch)

	if not switch then --switch is no longer nil but is instead "1"
		return
	end

	print("Hello") 
end

f(1)--prints "hello"

使用函數作為參數和回傳值

將函數作為參數或使用它們作回傳值是一個有用的功能,因為它讓你將自己的指令插入到現有的程式碼中。一個好的範例是 table.sort,它可以選擇性地使用自訂的「小於」函數

> list = {{3}, {5}, {2}, {-1}}
> table.sort(list)
attempt to compare two table values
stack traceback:
        [C]: in function 'sort'
        stdin:1: in main chunk
        [C]: in ?
> table.sort(list, function (a, b) return a[1] < b[1] end)
> for i,v in ipairs(list) do print(v[1]) end
-1
2
3
5

可變數量的引數

函數可以在其引數清單的末尾放置 ...。這將擷取命名後的剩餘參數。然後你可以在函數的內文中使用 ...,它將評估多個值(與有多重回傳值的函數呼叫相同規則)。

例如,一個函數不變地將其多餘的參數傳遞給另一個函數

> f = function (x, ...)
>>  x(...)
>> end
> f(print, "1 2 3")
1 2 3

這也是函數將另一個函數作為參數示例。

若要從 ... 取得特定項目,請使用 select 函數,它會取得一個數字和可變數量的參數,並從該索引開始傳回參數。它也可以將 "#" 作為索引,並傳回參數的數量

> f=function(...) print(select("#", ...)) print(select(3, ...)) end
> f(1, 2, 3, 4, 5)
5
3 4 5

... 也可以打包成一個表單

> f=function(...) tbl={...} print(tbl[2]) end
> f("a", "b", "c")
b

也可將具有陣列項目的表單「解壓縮」至參數清單

> f=function(...) tbl={...} print(table.unpack(tbl)) end -- it's just "unpack" (without the table.) in 5.1
> f("a", "b", "c")
a b c
> f("a", nil, "c") -- undefined result, may or may not be what you expect
但在第二個範例中,我們看到一個問題:表單不能儲存 nil,這意味著不能使用 # 算子(table.unpack 在內部使用它),因為如果陣列有 nil「洞」,它是不明確的。即使迴圈表單,以找到擁有最大金鑰的項目,仍然無法取得真正的長度,如果 nil 是傳給函數的最後參數。

Lua 5.2 新增了一個 table.pack 函數來協助解決此問題,其作用方式類似於 {...} ,但它會新增一個包含項目數量的「n」欄位

> f=function(...) tbl=table.pack(...) print(tbl.n, table.unpack(tbl, 1, tbl.n)) end
> f("a", "b", "c")
3 a b c
> f("a", nil, "c")
3 a nil c
我們在此也使用了 table.unpack 的開始和結束索引參數,如果給定這些參數,它將使用它們,而不是從 1 開始在 #tbl 結束。

「命名」函數的語法捷徑

儘管 Lua 讓我們自由地使用函數就像使用其他值一樣,但我們通常只會想給它們一個名稱(透過將它們儲存在變數中),並依該名稱使用它們。Lua 有一些語法糖,可讓儲存在變數中的函數看起來更漂亮

function f(...)
end

-- is equivalent to:

f = function (...)
end

這種語法本可以用本教程範例說明,但 = 符號讓變數為值的函式更加清楚。一般建議在實際指令碼中使用快捷語法,除非沒有要為函式命名之理由。

此外,也有類似的語法可將函式儲存在資料表中

function a.b.f(...)
end

-- is equivalent to:

a.b.f = function (...)
end

遞迴與尾呼叫

遞迴函式是指呼叫自身函式的函式。例如

function factorial(x)
  if x == 1 then
    return 1
  end
  return x * factorial(x-1)
end

也可有相互遞迴函式,例如 a 呼叫 bb 再呼叫 a,如此重複執行。

此類函式會有個問題,每次呼叫函式時,Lua 都必須記錄呼叫來源,才能知道返回位置。此資訊會儲存在稱為呼叫堆疊的資料結構,每呼叫一次函式就增加內容,每返回一次函式就減少內容。因此,當撰寫函式可以遞迴呼叫自身數千次時,會使堆疊大量增加。

解決方法是尾呼叫:假設函式回傳另一函式呼叫的精確、未變更結果,Lua 就知道不必再返回函式,可以重複使用目前堆疊槽,並讓所呼叫的函式直接返回呼叫目前函式的函式。

如果以上說明讓人困惑,也可採用另一種想法:尾呼叫只是跳躍,而非實際函式呼叫。

以下是以上函式的尾端遞迴範例

function factorial_helper(i, acc)
  if i == 0 then
    return acc
  end
  return factorial_helper(i-1, acc*i)
end

function factorial(x)
  return factorial_helper(x, 1)
end

以下是何謂尾呼叫與非尾呼叫的一些範例

return f(arg) -- tail call
return t.f(a+b, t.x) -- tail call
return 1, f() -- not a tail call, the function's results are not the only thing returned
return f(), 1 -- not a tail call, the function's results are not the only thing returned
return (f()) -- not a tail call, the function's possible multiple return values need to be cut down to 1 after it returns
return f() + 5 -- not a tail call, the function's return value needs to be added to 5 after it returns
return f().x -- not a tail call, the function's return value needs to be used in a table index expression after it returns

並非所有非尾呼叫的遞迴函式都不好,如果呼叫次數不會過多,這通常是最自然的方法。但假設會執行數百次以上的反覆執行時,應改考慮使用尾端遞迴,或直接使用迴圈。

最後,引進尾呼叫時,唯一理由是基於遞迴,因為這是最常會使用到的時候。但這並不表示尾呼叫僅限於呼叫同一個函式時使用,呼叫不同函式時依然有效。


RecentChanges · 喜好設定
編輯 · 過往記錄
上次編輯時間:格林威治標準時間 2016 年 6 月 23 日 下午 6:16 (diff)