編碼 · 程式語言
Lua 入門教學
簡答
Lua 是輕量、可嵌入的腳本語言,用於遊戲、Roblox(透過其 Luau 方言)、Neovim、Redis 與嵌入式裝置。它的語法精簡,只有一種資料結構 table,可同時當作陣列、字典與物件。本頁涵蓋安裝 Lua、變數與型別、流程控制、函式、table、metatable、用 pcall 處理錯誤,以及一個完整範例。
Lua 是什麼、可以拿來做什麼
Lua 是 1993 年由巴西大學 PUC-Rio 創造的小巧、快速的動態型別腳本語言。整個直譯器是一個以 C 寫成的精簡函式庫,因此很容易嵌入到較大的程式中。它只有一種資料結構 table 和很少的概念,所以學起來很快。
Lua 主要作為嵌入式腳本語言使用:遊戲引擎與遊戲(Roblox 使用名為 Luau 的 Lua 方言;LÖVE 與 Defold 使用 Lua)、Neovim 編輯器、Redis 資料庫、搭配 OpenResty 的 Nginx 等網路工具,以及許多嵌入式裝置。LuaJIT 是針對 Lua 5.1 程式碼的熱門即時編譯器。
安裝 Lua 並執行 Hello, world
用套件管理員安裝 Lua(例如 brew install lua 或 apt install lua5.4),或從 lua.org 的原始碼建置。單獨執行 lua 會進入互動式提示。
-- hello.lua
print("Hello, world!")lua hello.lua
# Hello, world!註解以 -- 開頭。區塊註解寫成 --[[ ... ]]。陳述式不需要分號。
變數與基本型別
變數預設是全域的。除非你真的想要全域變數,否則請一律用 local 宣告。Lua 有八種基本型別;日常會用到的列在下方。
local name = "Lua"
local year = 1993 -- integer
local pi = 3.14159 -- float
local ok = true
local nothing = nil
print(type(name), type(year), type(nothing)) -- string number nil
print(math.type(year), math.type(pi)) -- integer float
print(name .. " " .. year) -- Lua 1993 (.. joins strings)
print(#name) -- 3 (length)
print(7 / 2, 7 // 2, 7 % 2, 2 ^ 10) -- 3.5 3 1 1024.0
print(string.format("%.2f", pi)) -- 3.14
print(tonumber("42") + 1, tostring(10)) -- 43 10| 型別 | 範例 | 說明 |
|---|---|---|
| nil | nil | 沒有值;未設定的變數是 nil |
| boolean | true、false | 只有 false 與 nil 為假;0 和 "" 為真 |
| number | 42, 3.14 | 自 Lua 5.3 起分整數與浮點數子型別 |
| string | "hi"、'hi'、[[long]] | 不可變的位元組字串 |
| table | {1, 2, x = 3} | 唯一的資料結構 |
| function | function() end | 一等值 |
不等於運算子是 ~=,不是 !=。邏輯運算子是單字 and、or 與 not。
流程控制
區塊以關鍵字 end 結束。Lua 有 if/elseif/else、while、repeat ... until、數值 for,以及可搭配迭代器的泛型 for。
local score = 72
if score >= 90 then
print("A")
elseif score >= 70 then
print("B") -- B
else
print("C")
end
for i = 1, 3 do -- 1 to 3, inclusive
io.write(i, " ") -- 1 2 3
end
print()
for i = 10, 1, -4 do io.write(i, " ") end -- 10 6 2
print()
local n = 3
while n > 0 do n = n - 1 end
repeat
n = n + 1
until n >= 2
print(n) -- 2Lua 沒有 continue 陳述式;請搭配 ::continue:: 標籤使用 goto continue,或用 if 重新組織迴圈。
函式
函式是值。它們可以用 ... 接收不定數量的引數,也能回傳多個值。閉包會捕捉外層作用域的區域變數。
local function add(a, b)
return a + b
end
local function minmax(t)
local lo, hi = math.huge, -math.huge
for _, v in ipairs(t) do
lo, hi = math.min(lo, v), math.max(hi, v)
end
return lo, hi -- two return values
end
local function counter()
local count = 0
return function() -- a closure
count = count + 1
return count
end
end
print(add(2, 3)) -- 5
local lo, hi = minmax({4, 9, 1})
print(lo, hi) -- 1 9
local next_id = counter()
print(next_id(), next_id()) -- 1 2
local function sum(...)
local total = 0
for _, v in ipairs({...}) do total = total + v end
return total
end
print(sum(1, 2, 3)) -- 6table:陣列、字典與物件
table 是關聯式陣列,可以用 nil 以外的任何值作為索引。同一種結構可以當作陣列、字典、集合、模組或物件。依慣例,陣列從索引 1 開始。
local fruits = {"apple", "banana"}
table.insert(fruits, "cherry")
print(#fruits, fruits[1]) -- 3 apple
for i, f in ipairs(fruits) do -- array part, in order
print(i, f)
end
local ages = {ann = 31, bo = 25}
ages["cy"] = 40
ages.bo = nil -- remove a key
for k, v in pairs(ages) do -- all keys, order not defined
print(k, v)
end
table.sort(fruits, function(a, b) return a > b end)
print(table.concat(fruits, ", ")) -- cherry, banana, appleMetatable 與物件導向的 Lua
Metatable 是 Lua 的招牌特色。附加在 table 上的 metatable 會改變它的行為:__index 提供缺少的鍵(用於繼承),__add、__eq 和 __tostring 等欄位則可重載運算子。冒號語法 obj:method() 會把物件當作隱藏的第一個參數 self 傳入。
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
function Vector:length()
return math.sqrt(self.x ^ 2 + self.y ^ 2)
end
Vector.__add = function(a, b) return Vector.new(a.x + b.x, a.y + b.y) end
Vector.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end
local v = Vector.new(3, 4) + Vector.new(0, 0)
print(tostring(v)) -- (3, 4)
print(v:length()) -- 5.0Lua 也有協程(coroutine.create、coroutine.resume、coroutine.yield),讓函式可以暫停並在稍後恢復。遊戲腳本常用它們來實作橫跨多個影格的逐步行為。
錯誤處理
用 error(message) 拋出錯誤。用 pcall(受保護呼叫)捕捉它,它不會中止程式,而是回傳 false 加上錯誤。許多函式庫函式不會拋出錯誤,而是回傳 nil 與錯誤訊息,io.open 就是如此。
local function divide(a, b)
if b == 0 then
error("division by zero")
end
return a / b
end
local ok, result = pcall(divide, 10, 2)
print(ok, result) -- true 5.0
ok, result = pcall(divide, 1, 0)
print(ok, result) -- false script.lua:3: division by zero
local f, err = io.open("missing.txt", "r")
if not f then print("open failed: " .. err) end錯誤訊息中的檔名與行號取決於程式碼執行的位置。assert(value, message) 在 value 為 false 或 nil 時拋出錯誤,是檢查結果的簡短寫法。
完整的小程式:字數統計
這個程式用模式比對統計文字中的單字,依出現頻率排序並印出前三名。它用到字串模式、table、自訂比較函式的排序,以及字串格式化。
local text = [[
The cat and the dog. The dog sat,
and the cat ran.
]]
local counts = {}
for word in text:lower():gmatch("%a+") do
counts[word] = (counts[word] or 0) + 1
end
local list = {}
for word, n in pairs(counts) do
list[#list + 1] = {word = word, n = n}
end
table.sort(list, function(a, b)
if a.n ~= b.n then return a.n > b.n end
return a.word < b.word
end)
for i = 1, 3 do
print(string.format("%-4s %d", list[i].word, list[i].n))
end
-- the 4
-- and 2
-- cat 2接下來可以學什麼
- lua.org 上的 Lua Reference Manual,簡短而完整。
- Programming in Lua(Lua 作者之一 Roberto Ierusalimschy 著),第一版可免費線上閱讀。
- 你所用的宿主環境的文件,例如 Neovim 的 Lua 指南、LÖVE 或 Roblox 的 Luau 文件。
- LuaRocks,Lua 模組的套件管理員。
常見問題
為什麼 Lua 的陣列從 1 開始?
Lua 的設計者選擇從 1 開始的索引,以符合一般數學與非程式設計師的習慣。ipairs、# 和 table.insert 等標準函式庫函式都假設序列從 1 開始。
Lua 比 Python 快嗎?
標準 Lua 直譯器體積小,對動態語言來說通常很快,而 LuaJIT 在許多工作負載下快得多。實際速度取決於任務,請針對你自己的情況量測。
pairs 和 ipairs 有什麼差別?
ipairs 從索引 1 開始依序走訪陣列部分,直到遇到第一個 nil。pairs 則以未指定的順序走訪 table 中的每個鍵。
Luau 是什麼?
Luau 是由 Roblox 開發、衍生自 Lua 5.1 的語言。它加入了漸進式型別與效能改進,用於 Roblox 遊戲。
Lua 適合初學者嗎?
適合。它的語法精簡一致、概念很少,因此在遊戲模組製作與教學中很受歡迎。它的標準函式庫很精簡,所以更多功能要依靠宿主程式或模組。