编码 · 编程语言
Lua 入门教程
简短回答
Lua 是一门轻量、可嵌入的脚本语言,用于游戏、Roblox(使用其 Luau 方言)、Neovim、Redis 和嵌入式设备。它语法精简,只有一种数据结构——表(table),可同时充当数组、字典和对象。本页涵盖 Lua 的安装、变量与类型、流程控制、函数、表、元表、用 pcall 处理错误,以及一个完整示例。
Lua 是什么,用来做什么
Lua 是一门小巧、快速的动态类型脚本语言,1993 年诞生于巴西的大学 PUC-Rio。它的整个解释器是一个用 C 编写的精简库,因此很容易嵌入到更大的程序中。它只有表这一种数据结构,概念很少,学起来很快。
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 语句;可以用 goto continue 配合 ::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)) -- 6表:数组、字典与对象
表是关联数组,可以用除 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, apple元表与面向对象的 Lua
元表是 Lua 的标志性特性。附加到表上的元表会改变它的行为:__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错误消息中的文件名和行号取决于代码运行的位置。当 value 为 false 或 nil 时,assert(value, message) 会抛出错误,是检查结果的简便方法。
一个完整的小程序:词频统计
这个程序用模式匹配统计文本中的单词,按频率排序并输出前三名。它用到了字符串模式、表、自定义比较函数排序和字符串格式化。
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 参考手册,简短而完整。
- Roberto Ierusalimschy(Lua 的作者之一)所著的《Programming in Lua》,第一版可在线免费阅读。
- 你所用宿主环境的文档,如 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 以不确定的顺序访问表中的每个键。
Luau 是什么?
Luau 是由 Roblox 开发、派生自 Lua 5.1 的语言。它增加了渐进类型和性能改进,用于 Roblox 游戏。
Lua 适合初学者吗?
适合。它的语法小巧一致、概念很少,因此在游戏模组制作和教学中很受欢迎。它的标准库极简,更多功能需要依赖宿主程序或模块。