Lua 程式設計/函式

一個棧 是一個專案的列表,其中專案可以被新增(壓入)或刪除(彈出),並遵循後進先出原則,這意味著最後一個新增的專案將是第一個被刪除的專案。這就是為什麼這種列表被稱為棧:在一個棧中,你不能移除一個專案,除非先移除位於其頂部的專案。因此,所有操作都在棧的頂部進行。如果一個專案在另一個專案之後新增,則它位於另一個專案的上面;如果它在另一個專案之前新增,則它位於另一個專案的下面。
一個函式(也稱為子程式、過程、例程或子程式)是一系列執行特定任務的指令,並且可以在程式的其他地方呼叫,以便在需要執行該指令序列時執行。函式還可以接收值作為輸入,並在可能操作輸入或根據輸入執行任務後返回輸出。函式可以在程式的任何地方定義,包括在其他函式內部,並且也可以從程式的任何可以訪問它們的部分呼叫:函式,就像數字和字串一樣,是值,因此可以儲存在變數中,並且具有變數共有的所有屬性。這些特性使函式非常有用。
因為函式可以從其他函式中呼叫,所以 Lua 直譯器(讀取和執行 Lua 程式碼的程式)需要能夠知道當前正在執行的函式是由哪個函式呼叫的,以便在函式終止(沒有更多程式碼要執行)時,它可以返回到正確函式的執行。這是透過一個稱為呼叫棧的棧來完成的:呼叫棧中的每個專案都是一個呼叫其正上方棧中函式的函式,直到棧中的最後一個專案,即當前正在執行的函式。當一個函式終止時,直譯器使用棧的彈出操作來移除列表中的最後一個函式,然後返回到前一個函式。
函式有兩種型別:內建函式和使用者定義函式。內建函式 是 Lua 提供的函式,包括諸如您已經知道的print函式等函式。一些函式可以直接訪問,比如print函式,但其他的需要透過庫來訪問,比如math.random函式,它返回一個隨機數。使用者定義函式 是使用者定義的函式。使用者定義函式使用函式建構函式定義
local func = function(first_parameter, second_parameter, third_parameter)
-- function body (a function's body is the code it contains)
end
上面的程式碼建立了一個具有三個引數的函式,並將其儲存在變數func中。下面的程式碼與上面的程式碼完全相同,但使用了定義函式的語法糖
local function func(first_parameter, second_parameter, third_parameter)
-- function body
end
需要注意的是,當使用第二種形式時,可以從函式內部引用該函式,而使用第一種形式則無法做到這一點。這是因為local function foo() end轉換為local foo; foo = function() end而不是local foo = function() end。這意味著在第二種形式中foo是函式環境的一部分,而在第一種形式中則不是,這解釋了為什麼第二種形式使得能夠引用函式本身。
在這兩種情況下,都可以省略local關鍵字將函式儲存在全域性變數中。引數就像變數一樣,允許函式接收值。當一個函式被呼叫時,可以向其傳遞引數。然後,函式將接收它們作為引數。引數就像在函式開頭定義的區域性變數一樣,並將根據傳遞給函式呼叫的引數的順序依次賦值;如果缺少引數,則引數的值將為nil。以下示例中的函式將兩個數字相加並列印結果。因此,當代碼執行時,它將列印5。
local function add(first_number, second_number)
print(first_number + second_number)
end
add(2, 3)
函式呼叫通常採用name(arguments)的形式。但是,如果只有一個引數,並且它是一個表或字串,並且它不在變數中(意味著它是在函式呼叫中直接構造的,表示為字面量),則可以省略括號
print "Hello, world!"
print {4, 5}
前一個示例中的第二行程式碼將打印表的記憶體地址。當將值轉換為字串時(print函式會自動執行此操作),複雜型別(函式、表、使用者資料和執行緒)將更改為其記憶體地址。但是,布林值、數字和 nil 值將轉換為相應的字串。
在實踐中,術語引數和實參經常互換使用。在這本書中,以及在它們正確的含義中,引數和實參分別表示對應實參的值將被賦值到的名稱,以及傳遞給函式以賦值給引數的值。
函式可以接收輸入,對其進行操作並返回輸出。您已經知道它們如何接收輸入(引數)和操作它(函式體)。它們還可以透過返回任何型別的零個或多個值來提供輸出,這是使用return語句完成的。這就是為什麼函式呼叫既是語句又是表示式:它們可以被執行,但也可以被求值。
local function add(first_number, second_number)
return first_number + second_number
end
print(add(5, 6))
上面函式中的程式碼將首先定義函式add。然後,它將使用5和6作為值來呼叫它。該函式將對它們進行加法運算並返回結果,然後列印該結果。這就是為什麼上面的程式碼會列印11。一個函式也可以透過用逗號分隔計算這些值的表示式來返回多個值。
使用者可以定義自己的函式,並根據需要對其進行自定義。
除了獲取引數和返回值之外,使用者定義的函式還可以具有副作用,即由函式執行引起的變數或程式狀態的變化。[1]
錯誤有三種類型:語法錯誤、靜態語義錯誤和語義錯誤。語法錯誤發生在程式碼明顯無效時。例如,下面的程式碼將被 Lua 檢測為無效
print(5 ++ 4 return)
上面的程式碼沒有意義;不可能從中獲得任何含義。類似地,在英語中,“cat dog tree”在語法上無效,因為它沒有意義。它不遵循建立句子的規則。
靜態語義錯誤發生在程式碼有意義但仍然沒有意義時。例如,如果您嘗試將字串與數字相加,則會得到一個靜態語義錯誤,因為不可能將字串與數字相加
print("hello" + 5)
上面的程式碼遵循 Lua 的語法規則,但它仍然沒有意義,因為它不可能將字串與數字相加(除非字串表示數字,在這種情況下,它將被強制轉換為數字)。這可以比作英語中的句子“I are big”。它遵循英語建立句子的規則,但它仍然沒有意義,因為“I”是單數,“are”是複數。
最後,語義錯誤是在程式碼片段的含義與其建立者認為的含義不符時發生的錯誤。這些是最糟糕的錯誤,因為它們可能非常難以發現。Lua 總是會在出現語法錯誤或靜態語義錯誤時告訴您(這稱為丟擲錯誤),但它無法告訴您何時出現語義錯誤,因為它不知道您認為程式碼的含義是什麼。這些錯誤發生的頻率比大多數人認為的要高,並且查詢和糾正它們是許多程式設計師花費大量時間做的事情。
查詢錯誤並對其進行糾正的過程稱為除錯。大多數情況下,程式設計師會花費更多時間查詢錯誤而不是實際糾正錯誤。這對所有型別的錯誤都適用。一旦您知道問題是什麼,修復它通常很簡單,但有時,程式設計師可能會檢視一段程式碼幾個小時而找不到其中的錯誤。
丟擲錯誤是指指示程式碼存在問題(無論是由直譯器手動執行還是由直譯器(讀取和執行程式碼的程式)自動執行)。當給定的程式碼無效時,Lua 會自動執行此操作,但也可以使用error函式手動執行
local variable = 500
if variable % 5 ~= 0 then
error("It must be possible to divide the value of the variable by 5 without obtaining a decimal number.")
end
error 函式還有一個第二個引數,用於指示應該在哪個棧層級丟擲錯誤,但本書不會介紹它。assert 函式的功能與 error 函式相同,但只有當它的第一個引數計算結果為 nil 或 false 時才會丟擲錯誤,並且它沒有引數可以用來指定應該在哪個棧層級丟擲錯誤。例如,assert 函式在指令碼開頭很有用,可以檢查指令碼執行所需的庫是否可用。
可能很難理解為什麼有人會想要主動丟擲錯誤,因為程式中的程式碼在丟擲錯誤時會停止執行。但是,當函式使用不正確或程式未在正確的環境中執行時丟擲錯誤,通常可以幫助除錯程式碼的人員立即找到錯誤,而無需長時間盯著程式碼卻不知道問題出在哪裡。
有時,防止錯誤停止程式碼並改做其他事情(例如向用戶顯示錯誤訊息以便他可以向開發人員報告錯誤)可能很有用。這稱為異常處理(或錯誤處理),透過捕獲錯誤以防止其傳播並執行異常處理程式來處理錯誤。在不同的程式語言中,實現方法差異很大。在 Lua 中,它是使用受保護呼叫來實現的。[2] 它們被稱為受保護呼叫,因為在受保護模式下呼叫的函式在發生錯誤時不會停止程式。有兩個函式可以用來在受保護模式下呼叫函式
| 函式 | 描述 |
|---|---|
pcall(function, ...) |
在受保護模式下呼叫函式,並返回一個狀態程式碼(一個布林值,其值取決於是否丟擲錯誤)以及函式返回的值,或者如果函式因錯誤而停止,則返回錯誤訊息。可以透過在第一個引數(應該在受保護模式下呼叫的函式)之後將引數傳遞給 pcall 函式來向函式傳遞引數。 |
xpcall(function, handler, ...) |
與 pcall 做同樣的事情,但是當函數出錯時,它不會返回與 pcall 返回相同的值,而是使用這些值作為引數呼叫 handler 函式。然後可以使用 handler 函式(例如)來顯示錯誤訊息。與 pcall 函式一樣,可以透過傳遞給 xpcall 函式來向函式傳遞引數。 |
之前提到過呼叫棧,它是一個包含所有已呼叫函式的棧,按照呼叫順序排列。大多數語言(包括 Lua)中的呼叫棧都有最大大小。這個最大大小非常大,在大多數情況下都不需要擔心,但是如果沒有任何東西可以阻止函式無限次地呼叫自身(這稱為遞迴,這樣的函式稱為遞迴函式),那麼這些函式可能會達到此限制。這稱為棧溢位。當棧溢位時,程式碼會停止執行並丟擲錯誤。
可變引數函式(也稱為 vararg 函式)是可以接受可變數量引數的函式。可變引數函式在其引數列表的末尾用三個點 ("...") 表示。不適合引數列表中引數的引數不會被丟棄,而是透過 vararg 表示式提供給函式,該表示式也用三個點表示。vararg 表示式的值為一個值列表(而不是表),然後可以使用以下表達式將其放入表中以便更容易地操作:{...}。在 Lua 5.0 中,額外的引數不是透過 vararg 表示式提供,而是透過一個名為“arg”的特殊引數提供。以下函式是一個示例,它會將第一個引數新增到它接收的所有引數中,然後將它們全部加起來並列印結果
function add_one(increment, ...)
local result = 0
for _, number in next, {...} do
result = result + number + increment
end
end
無需理解上面的程式碼,它只是一個可變引數函式的演示。
select 函式可用於操作引數列表而無需使用表。它本身就是一個可變引數函式,因為它接受無限數量的引數。它返回第一個引數指定的數字後的所有引數(如果給定的數字為負數,則從末尾開始索引,這意味著 -1 是最後一個引數)。如果第一個引數是字串“#”,它還會返回它接收到的引數數量(不包括第一個)。它可以用來丟棄引數列表中某個數字之前的全部引數,並且更重要的是,可以區分作為引數傳送的 nil 值和未傳送任何值的情況。實際上,當“#”作為第一個引數給出時,select 會區分 nil 值和無值。引數列表(以及返回值列表)是元組的例項,將在關於表的章節中探討;select 函式適用於所有元組。
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)()) --> no value
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(nil)) --> nil
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(variable_that_is_not_defined)) --> nil
-- As this code shows, the function is able to detect whether the value nil was passed as an argument or whether there was simply no value passed.
-- In normal circumstances, both are considered as nil, and this is the only way to distinguish them.
- ↑ "Lua 函式 - 綜合指南及示例". 2023-03-22. 檢索於 2023-05-17.
- ↑ 更多資訊,請參閱:Ierusalimschy, Roberto (2003). "應用程式程式碼中的錯誤處理". Lua 程式設計 (第一版). Lua.org. ISBN 8590379817. 檢索於 2014年6月20日.