Lua 最特别的地方:table + 元表 统一了所有数据结构,数组、字典、对象、模块全用这一套。
Lua 是巴西 PUC-Rio 大学在 1993 年搞出来的轻量级脚本语言,用标准 C 写,编译完才一百多 K。设计目标是嵌入应用程序里做扩展。
主要使用场景:游戏开发(魔兽世界、愤怒的小鸟)、Web 应用脚本(OpenResty/Nginx)、数据库插件(Redis、MySQL Proxy)、安全工具。
核心特点:
-- hello.lua
print("Hello, Lua!")运行:
lua hello.lua # 脚本式
lua -i # 交互式,直接敲代码或者加 Shebang:
#!/usr/local/bin/lua
print("Hello World!")chmod +x hello.lua
./hello.lua安装(Linux):
# 源码编译
curl -L -R -O https://www.lua.org/ftp/lua-5.4.7.tar.gz
tar zxf lua-5.4.7.tar.gz && cd lua-5.4.7
make all test && make install
# 包管理器
sudo apt install lua5.3 # Debian/Ubuntu
brew install lua # macOS-- 单行注释
--[[
多行注释
多行注释
]]
-- 注意:Lua 不支持嵌套注释,--[[ 里面再写 --[[ ]] 会出错标识符规则:字母/下划线开头,后跟字母、下划线、数字,且区分大小写。
保留关键字(22 个):
and break do else elseif
end false for function goto
if in local nil not
or repeat return then true
until while注意:下划线加大写字母(如 _VERSION)被 Lua 内部保留,别用。
Lua 的变量默认全是全局的,除非用 local 声明。访问不存在的变量不会报错,返回 nil。
a = 5 -- 全局变量
local b = 10 -- 局部变量(推荐!更快、更安全)
function test()
c = 3 -- 没加 local,默认全局
local d = 7 -- 局部,函数外不可见
end
test()
print(c, d) --> 3 nil删除变量:赋值为 nil。
b = nil
print(b) --> nil (等同于不存在)多重赋值:
a, b = 10, 20
a, b = b, a -- 交换,不需要临时变量!
-- 数量不匹配时:
a, b, c = 0, 1 -- c 补 nil
a, b = 1, 2, 3 -- 多余的 3 被丢弃
a, b, c = 0 -- 0, nil, nil(常见错误,不会自动补0)
-- 函数多返回值直接接住
x, y = some_func()local a = 5 -- 从声明处到所在块结束
do
local b = 10 -- 仅在 do...end 块内可见
print(b) -- 10
end
-- print(b) -- nil两条建议:
Lua 是动态类型,变量不声明类型。一共 8 种:
| 类型 | 说明 |
| nil | 空值/无效值,条件判断中等价于 false |
| boolean | true和 false |
| number | 双精度浮点(double),没有 int/float 之分 |
| string | 字符串,单引号、双引号或 [[ ]]表示 |
| function | 函数是一等公民 |
| userdata | C 语言传入的任意数据结构 |
| thread | 协同程序(coroutine) |
| table | 万能容器:数组 + 哈希表 + 对象 + 模块 |
print(type("Hello")) --> string
print(type(10.4 * 3)) --> number
print(type(print)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type)) --> function-- 未初始化的变量值就是 nil
print(a) --> nil
-- 删除全局变量 / table 字段
a = nil
-- 注意:type 返回的是字符串 "nil",不是 nil
print(type(a) == nil) --> false
print(type(a) == "nil") --> trueLua 判假只有两个值:false 和 nil。其他所有值全是 true,包括 0 和空字符串 ""!
if 0 then
print("0 是 true") -- 会执行!
end
if not nil then
print("nil 是 false") -- 会执行
end只有一种数值类型,等价于 C 的 double。
print(type(2))
print(type(2.2))
print(type(2e+1)) -- 科学计数法
print(type(0.2e-1))
--> 全是 number
-- 算术运算时,字符串数字会自动转成数字
print("2" + 6) --> 8.0
print("2" + "6") --> 8.0
print("2 + 6") --> "2 + 6"(只是字符串)
print("error" + 1) --> 报错!不能转换三种写法:单引号、双引号、[[ ]](长字符串,可含换行)。
s1 = 'hello'
s2 = "world"
s3 = [[
<html>
<body>多行文本可以不转义</body>
</html>
]]
-- 字符串连接用 ..(不是 +)
print("Hello " .. "World") --> Hello World
print(157 .. 428) --> "157428"(自动转成字符串)
-- 取长度用 #
print(#"abcde") --> 5转义字符与 C 基本一致:\n、\t、\\、\"等。
| 运算符 | 说明 | 示例 |
| +-*/ | 加减乘除 | 3/2 → 1.5 |
| % | 取余 | 7%3 → 1 |
| ^ | 幂运算(右结合) | 2^3^2 → 2^(3^2) = 512 |
| - | 一元负号 | -x |
| // | 整除(Lua 5.3+) | 5//2 → 2 |
>、<、>=、<=、== 和 ~=。
注意:不等于是 ~=,不是 !=。
a and b -- a 为假返回 a,否则返回 b
a or b -- a 为真返回 a,否则返回 b
not a -- 取反
-- 常用于默认值设置
x = x or 100 -- 如果 x 是 nil/false,用默认 100
name = name or "匿名""a" .. "b" -- 字符串连接
#"hello" -- 取长度(字符串或表的数组部分长度)从高到低:
| 优先级 | 运算符 | 说明 | 结合性 |
| 13 | ^ | 幂运算 | 右结合 |
| 12 | -、# | 一元负号、取长度 | 右结合 |
| 11 | *、/、//、% | 乘、除、整除、取模 | 左结合 |
| 10 | +、- | 加、减 | 左结合 |
| 9 | .. | 字符串拼接 | 右结合 |
| 8 | <、>、<=、>=、~=、== | 比较运算 | 左结合 |
| 7 | not | 逻辑非(一元) | 右结合 |
| 6 | and | 逻辑与 | 左结合 |
| 5 | or | 逻辑或 | 左结合 |
| 1 | = | 赋值 | 右结合 |
注意:括号 () 优先级最高,可以强制改变运算顺序。
-- if 格式(注意 then 和 end 关键字)
if (a < 20) then
print("a 小于 20")
end
-- if...else
if (a < 20) then
print("a < 20")
else
print("a >= 20")
end
-- if...elseif...else
if (a == 10) then
print("a = 10")
elseif (a == 20) then
print("a = 20")
elseif (a == 30) then
print("a = 30")
else
print("没有匹配")
endLua 没有 switch,多分支用 if-elseif。
三种循环结构:
-- 1. while:条件为真时重复
while (a < 10) do
a = a + 1
end
-- 2. for(数值型):知道循环次数时用
for i = 1, 10 do -- 从1到10
print(i)
end
for i = 1, 10, 2 do -- 步长为2
print(i)
end
-- 3. repeat...until:至少执行一次,直到条件为真
repeat
a = a + 1
until (a > 10)-- goto 模拟 continue
for i = 1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, "no continue")
::continue::
print("i'm end")
end
-- 标签定义
::label::
goto label-- 语法:[scope] function name(args)
-- body
-- return values
-- end
function max(a, b)
if a > b then
return a
else
return b
end
end
local result = max(10, 4) --> 10function max_and_index(t)
local max_val = t[1]
local max_idx = 1
for i, v in ipairs(t) do
if v > max_val then
max_val, max_idx = v, i
end
end
return max_val, max_idx -- 返回两个值
end
val, idx = max_and_index({3, 9, 5, 2}) -- val=9, idx=2function add(...)
local sum = 0
local args = {...} -- 将所有参数打包成 table
for i, v in ipairs(args) do
sum = sum + v
end
return sum
end
print(add(3, 4, 5, 6, 7)) --> 25
-- select("#", ...) 获取参数个数
function average(...)
local sum = 0
for i, v in ipairs({...}) do sum = sum + v end
return sum / select("#", ...)
end-- 匿名函数赋给变量
fact = function (n)
if n == 0 then return 1
else return n * fact(n - 1) end
end
-- 函数作为参数传递
function testFun(tab, fun)
for k, v in pairs(tab) do
print(fun(k, v))
end
end
testFun({key1="a", key2="b"}, function(key, val)
return key .. "=" .. val
end)| 函数 | 说明 | 示例 |
string.len(s) | 长度 | string.len("abc")→ 3 |
string.upper(s) | 转大写 | "Lua"→ "LUA" |
string.lower(s) | 转小写 | "Lua"→ "lua" |
string.reverse(s) | 反转 | "Lua"→ "auL" |
string.sub(s, i, j) | 截取(从 1 开始) | sub("12345",2,4)→ "234" |
string.rep(s, n) | 重复 n 次 | rep("ab",2)→ "abab" |
string.char(...) | ASCII → 字符 | char(65,66)→ "AB" |
string.byte(s, i) | 字符 → ASCII | byte("ABC",1)→ 65 |
string.format(...) | 格式化(类似 C printf) | format("%.2f", 3.1415)→ "3.14" |
string.find(s, pat) | 查找,返回首尾位置 | find("hello","ll")→ 3, 4 |
string.gsub(s,pat,rep) | 替换 | gsub("aaaa","a","z",2)→ "zzaa" |
string.match(s, pat) | 匹配第一个结果 | — |
string.gmatch(s, pat) | 返回匹配迭代器 | 用于 for 循环 |
-- 字符串截取
local s = "prefix--runoobgoogletaobao--suffix"
print(string.sub(s, 4, 15)) --> fix--runoobg
print(string.sub(s, -10)) --> ao--suffix(后10个)
-- 格式化
string.format("%05d", 17) --> "00017"
string.format("%.4f", 1/3) --> "0.3333"
string.format("%q", "One\nTwo") --> '"One\nTwo"'Lua 的"正则"是自己的模式匹配系统,比 PCRE 简单但够用。
-- 提取日期
s = "Deadline is 30/05/1999, firm"
pattern = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, pattern))) --> 30/05/1999常用字符类:
| 类 | 匹配 |
. | 任意字符 |
%a | 字母 |
%d | 数字 |
%s | 空白符 |
%w | 字母/数字 |
%l / %u | 小写/大写字母 |
%p | 标点符号 |
%c | 控制字符 |
%x | 十六进制数字 |
大写表示取反:%D= 非数字,%S= 非空白。
-- 匹配 email
for word in string.gmatch("a@x.com b@y.org", "%w+@%w+%.%w+") do
print(word)
end修饰符:*(0或多个,贪心)、+(1或多个)、-(0或多个,非贪心)、?(0或1个)。
捕获用 (),捕获内容可在 gsub 里用 %1%2引用。
Lua 没有专门的数组类型,数组就是用 table 模拟的。索引默认从 1 开始(和大部分语言不一样!)。
-- 创建
local arr = {10, 20, 30, 40, 50}
print(arr[1]) --> 10
print(arr[3]) --> 30
print(#arr) --> 5(长度)
-- 遍历
for i = 1, #arr do
print(arr[i])
end
-- 末尾追加
arr[#arr + 1] = 60
-- table 库操作
table.insert(arr, 3, 25) -- 在位置 3 插入 25
table.remove(arr, 3) -- 删除位置 3 的元素
table.sort(arr) -- 排序索引可以不是从 1 开始,甚至可以是负数:
local arr = {}
for i = -2, 2 do
arr[i] = i * 2
end
print(arr[-2]) --> -4
print(arr[2]) --> 4多维数组:table 里套 table。
local matrix = {}
for i = 1, 3 do
matrix[i] = {}
for j = 1, 3 do
matrix[i][j] = i * j
end
end
print(matrix[2][3]) --> 6Table 是 Lua 的终极数据结构 —— 数组 + 哈希表 + 对象 + 模块,一个 table 全搞定。
-- 空表
local t = {}
-- 数组形式(默认 key 为 1, 2, 3...)
local t = {"apple", "pear", "orange"}
-- 字典形式
local t = {key1 = "val1", key2 = "val2"}
-- 混合
local t = {name = "Tom", 10, 20, 30}t[1] -- 用数字索引
t["key1"] -- 用字符串索引
t.key1 -- 字符串键的语法糖(等价于 t["key1"])
-- 访问不存在的键返回 nil
print(t["none"]) --> nillocal a = {1, 2, 3}
local b = a -- b 和 a 指向同一个 table
b[1] = 100
print(a[1]) --> 100(a 也被改了)
a = nil -- 只清掉变量 a,b 还能访问 table
-- 等 b 也被 nil 时,GC 才会回收-- 连接
table.concat({"a","b","c"}, ",") --> "a,b,c"
table.concat({"a","b","c"}, ",", 2, 3) --> "b,c"
-- 插入/删除
table.insert(t, val) -- 末尾插入
table.insert(t, pos, val) -- 指定位置插入
table.remove(t) -- 删除最后一个
table.remove(t, pos) -- 删除指定位置
-- 排序
table.sort(t) -- 升序
table.sort(t, function(a, b) return a > b end) -- 降序注意:#t只在数组部分"连续"时可靠。如果中间有空洞(nil),#的行为是不确定的。要取真实长度建议遍历:
function table_len(t)
local n = 0
for k, v in pairs(t) do n = n + 1 end
return n
end-- ipairs:遍历数组部分,从 1 到第一个 nil 为止
local t = {10, 20, 30, key = "val"}
for i, v in ipairs(t) do
print(i, v) -- 1 10, 2 20, 3 30(key="val" 不输出)
end
-- pairs:遍历所有键值对(顺序不定)
for k, v in pairs(t) do
print(k, v) -- 输出所有的
end-- 闭包方式
function elementIterator(tbl)
local index = 0
return function()
index = index + 1
if index <= #tbl then
return tbl[index]
end
end
end
for elem in elementIterator({"a", "b", "c"}) do
print(elem)
end-- module.lua —— 一个模块就是一个返回 table 的文件
local M = {} -- 建议用 local
M.constant = "这是一个常量"
function M.func1()
print("公有函数")
end
local function func2() -- local 表示私有,外部不可见
print("私有函数")
end
function M.func3()
func2()
end
return M -- 返回-- 加载模块
local m = require("module") -- require 返回模块 table
print(m.constant) --> 这是一个常量
m.func3() --> 私有函数require 加载机制:
package.path中按顺序搜索 .lua文件package.cpath中搜索 .so/.dll(C 扩展)package.loaded自定义搜索路径:
export LUA_PATH="~/lua/?.lua;;"元表是 Lua 对象系统的核心。通过元表,可以重载操作符、控制对表的访问。
-- 设置元表
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t)) -- 返回 mt-- 让表有"继承"行为
local parent = {name = "parent", age = 100}
local child = setmetatable({}, {__index = parent})
print(child.name) --> "parent"(从 parent 表中找到)
-- __index 也可以是函数
local t = setmetatable({key1 = "val1"}, {
__index = function(tbl, key)
if key == "key2" then return "default2" end
end
})
print(t.key1, t.key2) --> val1 default2local mt = {}
local t = setmetatable({key1 = "val1"}, {__newindex = mt})
t.newkey = "new" -- 新键 → 触发 __newindex,值存到 mt
print(t.newkey, mt.newkey) --> nil new
t.key1 = "changed" -- 已存在的键 → 直接赋值,不触发
print(t.key1, mt.key1) --> changed nil
-- 配合 rawset 实现拦截
t = setmetatable({key1 = "val1"}, {
__newindex = function(tbl, key, value)
rawset(tbl, key, '"' .. value .. '"') -- 绕过元表直接赋值
end
})
t.key2 = 4
print(t.key2) --> "4"-- 两个 table 相加
local mt = {
__add = function(a, b)
local result = {}
for _, v in ipairs(a) do table.insert(result, v) end
for _, v in ipairs(b) do table.insert(result, v) end
return result
end
}
local t1 = setmetatable({1, 2, 3}, mt)
local t2 = {4, 5, 6}
local t3 = t1 + t2 -- {1, 2, 3, 4, 5, 6}| 元方法 | 运算符 | 元方法 | 运算符 |
__add | + | __sub | - |
__mul | * | __div | / |
__mod | % | __unm | -(一元) |
__concat | .. | __eq | == |
__lt | < | __le | <= |
local t = setmetatable({1, 2, 3}, {
__call = function(tbl, ...)
local sum = 0
for _, v in ipairs(tbl) do sum = sum + v end
for _, v in ipairs({...}) do sum = sum + v end
return sum
end
})
print(t(10, 20, 30)) --> 66(1+2+3 + 10+20+30)local t = setmetatable({10, 20, 30}, {
__tostring = function(tbl)
local sum = 0
for _, v in ipairs(tbl) do sum = sum + v end
return "表元素之和为 " .. sum
end
})
print(t) --> 表元素之和为 60非抢占式"微线程",手动切换控制权,适合协作式多任务。
| 函数 | 说明 |
coroutine.create(f) | 创建协程,参数是一个函数 |
coroutine.resume(co, ...) | 启动/恢复协程,可传参 |
coroutine.yield(...) | 挂起协程,返回值给 resume |
coroutine.status(co) | 状态:running/ suspended/ dead |
coroutine.wrap(f) | 类似 create,但返回一个函数而非协程对象 |
coroutine.running() | 返回当前运行的协程 |
local function foo()
print("foo 开始")
local value = coroutine.yield("暂停") -- 挂起,返回 "暂停"
print("foo 恢复,收到: " .. value)
end
local co = coroutine.create(foo)
local ok, result = coroutine.resume(co) -- 启动
print(result) --> 暂停
ok, result = coroutine.resume(co, 42) -- 恢复并传入 42
-- 输出:foo 恢复,收到: 42local co
function send(x)
coroutine.yield(x) -- 挂起自己,把数据送出去
end
function receive()
local ok, value = coroutine.resume(co)
return value
end
co = coroutine.create(function()
local i = 0
while true do
i = i + 1
send(i) -- 生产数据
end
end)
-- 消费
for _ = 1, 5 do
print(receive()) -- 1 2 3 4 5
end-- 读
file = io.open("test.txt", "r")
io.input(file)
print(io.read()) -- 读一行
io.close(file)
-- 写
file = io.open("test.txt", "a")
io.output(file)
io.write("-- 尾部追加注释\n")
io.close(file)local file = io.open("test.txt", "r")
print(file:read()) -- 读一行
file:close()
file = io.open("test.txt", "a")
file:write("-- appending\n")
file:close()| 模式 | 说明 |
"r" | 只读,文件必须存在 |
"w" | 只写,存在则清空,不存在则创建 |
"a" | 追加写 |
"r+" | 可读写,文件必须存在 |
"w+" | 可读写,存在则清空 |
"a+" | 读写 + 追加 |
"b" | 二进制模式(附加在以上模式后,如 "rb") |
file:read("*n") -- 读一个数字
file:read("*a") -- 读整个文件
file:read("*l") -- 读一行(默认)
file:read(10) -- 读 10 个字符file:seek("set", 0) -- 移动到开头
file:seek("cur", 10) -- 从当前位置移 10 字节
file:seek("end", -25) -- 从尾部往前 25 字节
file:seek() -- 返回当前位置for line in io.lines("test.txt") do
print(line)
endlocal function add(a, b)
assert(type(a) == "number", "a 不是数字")
assert(type(b) == "number", "b 不是数字")
return a + b
end
add(10) -- 报错: b 不是数字error("自定义错误信息") -- level=1,在 error 位置报错
error("错误", 2) -- level=2,在上层调用处报错local ok, result = pcall(function()
return 10 / 0
end)
if ok then
print("成功:", result)
else
print("出错:", result) -- result 是错误信息
endlocal function handler(err)
print("捕获错误:", err)
print(debug.traceback()) -- 打印调用栈
end
local ok = xpcall(
function() error("出错了!") end,
handler
)debug.traceback("自定义消息") -- 打印调用栈
debug.getinfo(1) -- 返回当前函数的信息表
debug.getupvalue(func, i) -- 获取闭包/函数的第 i 个 upvalue
debug.setupvalue(func, i, val) -- 修改 upvalue
debug.getlocal(level, i) -- 获取局部变量
debug.setlocal(level, i, val) -- 修改局部变量Lua 是自动内存管理,GC 是递增式标记-扫描。一般不用手动干预:
collectgarbage("collect") -- 立即执行一次完整 GC
collectgarbage("count") -- 查看已用内存(KB)
collectgarbage("stop") -- 暂停自动 GC
collectgarbage("restart") -- 恢复自动 GC
collectgarbage("setpause", 150) -- 设置间歇率
collectgarbage("setstepmul", 300)-- 设置步进倍率Lua 没有 class 关键字,OOP 靠 table + 元表模拟:
-- 定义"类"
Rectangle = {area = 0, length = 0, breadth = 0}
function Rectangle:new(o, length, breadth)
o = o or {}
setmetatable(o, self)
self.__index = self
self.length = length or 0
self.breadth = breadth or 0
self.area = length * breadth
return o
end
function Rectangle:printArea()
print("矩形面积:", self.area)
end
-- 使用
local r = Rectangle:new(nil, 10, 20)
r:printArea() --> 矩形面积: 200冒号语法:function obj:method(args)等价于 function obj.method(self, args),自动传入 self。
继承:
Shape = {area = 0}
function Shape:new(o, side)
o = o or {}
setmetatable(o, self)
self.__index = self
self.area = side * side
return o
end
function Shape:printArea()
print("面积:", self.area)
end
-- Square 继承 Shape
Square = Shape:new() -- 创建 Shape 实例作为 Square 的元表
function Square:new(o, side)
o = o or Shape:new(o, side)
setmetatable(o, self)
self.__index = self
return o
end
-- 重写方法
function Square:printArea()
print("正方形面积:", self.area)
end
local sq = Square:new(nil, 10)
sq:printArea() --> 正方形面积: 100{"a","b"}的索引是 1、2,不是 0、1if 0 then ...会执行,只有 nil和 false是假~=,不是 !=#操作用于有 nil 空洞的 table 时不可靠,遍历要真实长度用 pairs计数local是常见 bugtab.key只对字符串键有效,tab[1]不能写成 tab.1..,不能用 +continue,用 goto代替--[[里面不能再有 --[[__index实现继承,__newindex实现拦截Lua 的灵魂就是「简单 + 可嵌入」。table 一把梭解决了所有数据结构问题,元表提供了强大的行为定制能力。学 Lua 的关键是理解 table 和 metatable 这套机制,其他语法基本上是 C 系的熟悉面孔。