一、Lua 简介
Lua 是一门轻量、小巧、可嵌入的脚本语言,1993 年诞生于巴西里约热内卢天主教大学。它的设计目标是作为一种"胶水语言",方便地嵌入到宿主程序(通常是 C/C++ 程序)中,为其提供灵活的扩展能力。
核心特点:
- 轻量:解释器极小(约 200KB),启动快,内存占用低
- 可嵌入:通过 C API 与宿主程序双向调用,极易集成
- 动态类型:变量无需声明类型,运行时确定
- 表(Table)为核心:数组、字典、对象、模块、命名空间都用表实现
- 函数是一等公民:函数可作为变量、参数、返回值,支持闭包
- 协程支持:语言内置非对称协程,适合异步/状态机
- 自动内存管理:基于标记清除的 GC
典型应用场景:
| 领域 |
案例 |
| 游戏开发 |
《魔兽世界》插件、ROBLOX、LÖVE 引擎、Unity 的 xLua / ToLua |
| Web 服务 |
OpenResty(Nginx + Lua)、Kong 网关 |
| 数据库 |
Redis 脚本、Tarantool |
| 嵌入式 |
路由器、机顶盒、Wireshark 的脚本 |
| 编辑器/工具 |
Neovim、AwesomeWM、Pandoc 过滤器 |
对游戏开发者来说,Lua 最大的价值在于热更新:把易变的业务逻辑(技能、活动、数值)写成 Lua,可以在线替换脚本而无需重新打包发版。
二、环境搭建与运行
安装:从 lua.org 下载源码编译,或使用发行版包管理器(apt install lua5.4、brew install lua)。游戏项目常使用 LuaJIT(兼容 Lua 5.1,带 JIT 编译,性能更高)。
检查版本:
运行代码的几种方式:
1
2
3
|
lua script.lua # 运行脚本文件
lua -e "print('hello')" # 直接执行一段代码
lua # 进入交互式 REPL(Ctrl+D 退出)
|
1
2
|
-- script.lua
print("Hello, Lua!") -- 输出:Hello, Lua!
|
注释:
1
2
3
4
5
6
7
8
9
10
11
|
-- 单行注释
--[[
多行注释
可以跨越多行
]]
--[==[
当注释内容里含有 ]] 时,
可以用更多等号作为长括号边界
]==]
|
三、基础语法
3.1 变量与作用域
Lua 的变量分为全局变量和局部变量。默认声明的变量是全局的,用 local 显式声明的是局部变量。
强烈建议:凡是能用局部变量的地方就用 local。局部变量访问更快(走寄存器),且不会污染全局环境。
1
2
3
4
5
6
7
8
9
10
|
a = 10 -- 全局变量(不推荐)
local b = 20 -- 局部变量(推荐)
do
local c = 30 -- 只在 do...end 块内可见
end
-- print(c) -- 错误:c 在此处为 nil
local x, y = 1, 2 -- 同时声明多个
local z -- 未赋值,默认 nil
|
命名规范:
- 变量、函数:小驼峰
local userName = "张三"
- 常量:全大写下划线
local MAX_LEVEL = 99
- 私有成员:下划线开头
local _internal = {}
- 避免用
_ 之外的下划线开头命名(如 _G、_ENV 是保留用途)
3.2 数据类型
Lua 有 8 种基本类型,用 type() 可以获取。
| 类型 |
说明 |
示例 |
nil |
空值,表示"无" |
local a → nil |
boolean |
布尔,只有 true / false |
local b = true |
number |
数字(5.3+ 区分整数/浮点,5.1 只有 double) |
10、3.14 |
string |
字符串(不可变) |
"hello"、'hi' |
table |
表,唯一的数据结构 |
{1, 2, 3} |
function |
函数 |
function f() end |
userdata |
C 侧数据(如 Unity 对象) |
由 C 创建 |
thread |
协程 |
coroutine.create(...) |
1
2
3
4
5
6
7
8
9
10
|
print(type(nil)) -- nil
print(type(true)) -- boolean
print(type(10)) -- number
print(type("abc")) -- string
print(type({})) -- table
print(type(print)) -- function
-- 只有 nil 和 false 为假,0 和 "" 都是真!
if 0 then print("0 是真的") end
if "" then print("空串也是真的") end
|
类型转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
print(tonumber("123")) -- 123
print(tonumber("3.14")) -- 3.14
print(tonumber("abc")) -- nil(转换失败)
print(tonumber("FF", 16)) -- 255(按十六进制解析)
print(tostring(123)) -- "123"
print(tostring(nil)) -- "nil"
-- 字符串与数字在算术中会自动转换
print("10" + 5) -- 15
print("3" * "4") -- 12
-- 但比较时不会自动转换
print("10" == 10) -- false
|
3.3 运算符
算术运算符:+ - * / % ^,以及 5.3+ 的整除 //。
1
2
3
4
|
print(7 / 2) -- 3.5(Lua 的 / 总是浮点除法)
print(7 // 2) -- 3(整除,5.3+)
print(7 % 2) -- 1
print(2 ^ 10) -- 1024.0(幂运算)
|
关系运算符:== ~= < > <= >=(注意"不等于"是 ~= 而不是 !=)。
逻辑运算符:and or not,具有短路特性,且返回的是操作数本身而非 true/false。
1
2
3
4
5
6
7
8
9
|
-- and / or 返回操作数,可用于默认值
local name = nil
local display = name or "默认名" -- "默认名"
local ok = true and "成功" -- "成功"
-- 模拟三目运算符:条件 and 真值 or 假值
local age = 20
local tag = (age >= 18) and "成年" or "未成年" -- "成年"
-- 注意:当真值为 false/nil 时会失效,需用表或函数替代
|
连接运算符:..,把两个字符串拼接(数字会自动转为字符串)。
1
2
3
4
|
print("a" .. "b") -- ab
print("n=" .. 10) -- n=10
print(1 .. 2 .. 3) -- 123
-- 效率提示:循环内大量拼接会产生很多临时字符串,建议用 table.concat
|
长度运算符:#,取字符串长度或序列长度。
1
2
|
print(#"hello") -- 5
print(#{1, 2, 3}) -- 3
|
运算符优先级(由高到低):
1
2
3
4
5
6
7
8
|
^
not # -(一元负号)
* / // %
+ -
..(右结合)
< > <= >= ~= ==
and
or
|
3.4 流程控制
if / elseif / else:
1
2
3
4
5
6
7
8
|
local score = 85
if score >= 90 then
print("优秀")
elseif score >= 60 then
print("及格")
else
print("不及格")
end
|
while:
1
2
3
4
5
|
local i = 0
while i < 3 do
print(i)
i = i + 1 -- Lua 没有 i++ / i += 1
end
|
repeat…until(至少执行一次):
1
2
3
4
|
local n = 0
repeat
n = n + 1
until n >= 3 -- 条件为真时退出
|
数值 for:
1
2
3
4
5
6
7
|
for i = 1, 5 do -- 1 到 5,步长默认 1
print(i)
end
for i = 10, 1, -2 do -- 起始、结束、步长
print(i) -- 10, 8, 6, 4, 2
end
|
泛型 for(遍历迭代器,后面 Table 章节详解):
1
2
3
|
for index, value in ipairs({ "a", "b", "c" }) do
print(index, value)
end
|
break 与 goto:break 跳出当前循环;goto label 跳转(Lua 5.2+,LuaJIT/5.1 不支持)。Lua 没有 continue,可用 goto 或反向 if 模拟。
1
2
3
4
5
|
for i = 1, 5 do
if i == 3 then goto continue end
print(i)
::continue::
end
|
四、函数
4.1 基本用法
函数用 function 定义,本质是一个值,可赋给变量、作为参数传递、作为返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- 标准定义
local function add(a, b)
return a + b
end
-- 等价写法(语法糖)
local add2 = function(a, b) return a + b end
print(add(1, 2)) -- 3
-- 函数作为参数(高阶函数)
local function apply(f, a, b)
return f(a, b)
end
print(apply(add, 3, 4)) -- 7
|
4.2 多返回值
Lua 函数可以返回多个值,这是它非常有用的特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
local function minMax(list)
local min, max = list[1], list[1]
for _, v in ipairs(list) do
if v < min then min = v end
if v > max then max = v end
end
return min, max
end
local lo, hi = minMax({3, 1, 4, 1, 5}) -- lo=1, hi=5
print(lo, hi)
local x, y, z = minMax({3, 1, 4}) -- z 为 nil
|
多返回值在调整时只保留第一个(放入括号或位于表达式中间时):
1
2
3
4
5
6
7
|
local function f() return 1, 2, 3 end
print(f()) -- 1 2 3
print((f())) -- 1(加括号只取第一个)
print(f(), 9) -- 1 9(不是最后一个位置只取第一个)
local t = { f() } -- {1, 2, 3}(列表构造时全部展开)
local t2 = { f(), 9 } -- {1, 9}
|
4.3 可变参数
用 ... 接收不定数量的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
local function sum(...)
local total = 0
for _, v in ipairs({ ... }) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4)) -- 10
-- 用 select 处理可变参数
local function count(...)
return select("#", ...) -- 参数个数
end
local function second(...)
return select(2, ...) -- 从第 2 个开始的全部参数
end
print(count(1, nil, 3)) -- 3(# 也能统计 nil)
print(second("a", "b", "c")) -- b c
|
4.4 默认参数与命名参数
Lua 不支持默认参数和命名参数,但可用惯用法模拟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- 默认参数:用 or 提供默认值
local function greet(name, greeting)
name = name or "世界"
greeting = greeting or "你好"
print(greeting .. "," .. name)
end
greet() -- 你好,世界
-- 命名参数:传入一个 table
local function createWindow(opts)
local title = opts.title or "无标题"
local width = opts.width or 800
local height = opts.height or 600
print(title, width, height)
end
createWindow({ title = "主窗口", width = 1024 })
|
4.5 闭包
闭包是函数 + 其捕获的外部局部变量。即使外层函数返回,被捕获的变量依然存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
local function counter()
local count = 0
return function()
count = count + 1
return count
end
end
local c1 = counter()
print(c1()) -- 1
print(c1()) -- 2
local c2 = counter()
print(c2()) -- 1(独立的新闭包)
|
1
2
3
4
5
6
7
8
9
10
11
12
|
-- 闭包在游戏中的典型用法:状态机、防抖、迭代器
local function makeIterator(t)
local i = 0
return function()
i = i + 1
return t[i]
end
end
local it = makeIterator({ "a", "b", "c" })
print(it()) -- a
print(it()) -- b
|
4.6 尾调用优化
当函数调用是函数的最后一个动作时,Lua 会进行尾调用优化,不增加栈深度(类似尾递归优化)。这对递归算法很重要。
1
2
3
4
5
6
7
8
9
10
11
|
-- 尾调用:返回的是函数调用,可优化
local function loop(n)
if n == 0 then return end
return loop(n - 1) -- 尾调用,不会栈溢出
end
-- 非尾调用:n * f(n-1) 中调用后还要做乘法
local function fact(n)
if n <= 1 then return 1 end
return n * fact(n - 1) -- 普通递归,n 大时会栈溢出
end
|
五、表(Table)
表是 Lua 中唯一的数据结构,既可以当数组,也可以当字典(哈希表),还能模拟对象、模块、队列、栈。理解表就理解了 Lua 的一半。
5.1 构造与访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- 数组式(下标从 1 开始!)
local arr = { 10, 20, 30 }
print(arr[1]) -- 10
print(arr[3]) -- 30
print(arr[0]) -- nil(Lua 没有 0 下标)
-- 字典式
local person = { name = "张三", age = 25 }
print(person.name) -- 张三
print(person["age"]) -- 25(等价写法)
-- 混合
local mixed = {
1, 2, 3, -- 1,2,3 为数组部分
name = "test", -- 键值部分
[100] = "hundred", -- 显式键
}
|
约定:数组和字典混用时,数组部分应从 1 开始连续,避免出现"空洞",否则 # 长度不可预测。
5.2 增删改查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
local t = {}
t.name = "Lua" -- 新增/修改
t["version"] = 5.4
print(t.name) -- Lua
t.name = nil -- 删除:置为 nil
print(t.name) -- nil
-- 数组操作
local list = { "a", "b", "c" }
list[#list + 1] = "d" -- 尾部追加(惯用写法)
table.insert(list, "e") -- 用 table 库追加
table.insert(list, 1, "z") -- 在索引 1 处插入
table.remove(list, 1) -- 删除索引 1 处元素
print(#list)
|
5.3 遍历:ipairs 与 pairs
| 迭代器 |
用途 |
顺序 |
ipairs |
遍历数组部分(从 1 开始、连续整数键) |
有序(1,2,3…) |
pairs |
遍历所有键值对 |
无序(哈希顺序) |
1
2
3
4
5
6
7
8
9
10
11
12
|
local arr = { "a", "b", "c" }
-- ipairs:遇到第一个 nil 就停止
for i, v in ipairs(arr) do
print(i, v) -- 1 a / 2 b / 3 c
end
-- pairs:遍历全部键(含非整数键)
local person = { name = "张三", age = 25, city = "北京" }
for k, v in pairs(person) do
print(k, v) -- 顺序不保证
end
|
坑:遍历过程中不要新增键(未定义行为),删除当前键在 pairs 中允许但需谨慎。
5.4 table 库常用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
local t = { 3, 1, 4, 1, 5 }
table.insert(t, 9) -- 尾部插入 → {3,1,4,1,5,9}
table.remove(t) -- 删除并返回末尾 → 9
table.sort(t, function(a, b) return a < b end) -- 排序
print(table.concat({ "a", "b", "c" }, "-")) -- a-b-c
print(table.unpack({ 1, 2, 3 })) -- 1 2 3(5.2+,5.1 用 unpack)
-- 深拷贝(浅拷贝直接赋值即可,table 是引用类型)
local function deepCopy(orig)
if type(orig) ~= "table" then return orig end
local copy = {}
for k, v in pairs(orig) do
copy[k] = deepCopy(v)
end
return copy
end
-- 经典:数组洗牌
math.randomseed(os.time())
for i = #t, 2, -1 do
local j = math.random(i)
t[i], t[j] = t[j], t[i] -- 平行赋值实现交换
end
|
注意:t = {} 只是让 t 指向新表,若有其他变量引用同一张表,它们仍指向旧表。
1
2
3
4
5
6
7
8
9
|
local a = { 1, 2, 3 }
local b = a -- b 和 a 是同一张表(引用)
b[1] = 100
print(a[1]) -- 100
local c = { 1, 2, 3 }
local d = c
c = {} -- c 指向新表,d 仍指向旧表
print(d[1]) -- 1
|
元表允许我们改变表的行为——定义表的加法、比较、索引、调用等操作。这是 Lua 面向对象和高级用法的基础。
1
2
3
4
5
6
7
8
|
local t = {}
local mt = {} -- 元表也是一个普通表
setmetatable(t, mt) -- 设置元表
print(getmetatable(t) == mt) -- true
-- 保护元表:外部不可修改
-- setmetatable(t, { __metatable = "locked" })
-- print(getmetatable(t)) -- "locked"
|
6.2 常用元方法
| 元方法 |
触发时机 |
__index |
访问不存在的键时 |
__newindex |
设置不存在的键时 |
__add / __sub / __mul / __div |
算术运算 |
__eq / __lt / __le |
比较运算 |
__tostring |
tostring() 或 print() 时 |
__call |
把表当函数调用时 |
__len |
对表用 # 时(5.2+) |
__concat |
用 .. 连接时 |
6.3 __index:实现继承与默认值
__index 是最重要的元方法,用于查找缺失的键。它可以是表(从该表继续查)或函数。
1
2
3
4
5
6
7
8
9
10
11
12
|
-- 用法一:默认值
local defaults = { color = "red", size = 10 }
local config = setmetatable({}, { __index = defaults })
print(config.color) -- red(自身没有,去 defaults 找)
-- 用法二:函数形式
local t = setmetatable({}, {
__index = function(tbl, key)
return "未定义的键:" .. tostring(key)
end
})
print(t.foo) -- 未定义的键:foo
|
1
2
3
4
5
6
7
|
-- 用法三:只读表
local protected = setmetatable({}, {
__newindex = function()
error("不允许修改只读表", 2)
end,
})
-- protected.x = 1 -- 报错
|
6.4 运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
function Vector.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
function Vector.__call(v, scale)
return Vector.new(v.x * scale, v.y * scale)
end
local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
print(v1 + v2) -- (4, 6)
print(v1 == Vector.new(1, 2)) -- true
print(v1(2)) -- (2, 4)
|
注意:__eq 只有在两个操作数类型相同且元表相同时才会触发;__lt/__le 可混合类型,但需都定义了元方法。
七、模块与包
Lua 用表来实现模块,用 require 加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- mymod.lua
local M = {} -- 模块表
function M.add(a, b)
return a + b
end
function M.greet(name)
return "Hello, " .. name
end
-- 私有函数(外部不可访问)
local function helper() end
return M -- 返回模块表
|
1
2
3
4
5
6
7
8
|
-- main.lua
local mymod = require("mymod") -- 首次加载后会被缓存
print(mymod.add(1, 2)) -- 3
print(mymod.greet("Lua")) -- Hello, Lua
-- 再次 require 返回同一份缓存,不会重新执行
local same = require("mymod")
print(same == mymod) -- true
|
包搜索路径:package.path 决定 require 去哪找 .lua 文件。
1
2
3
|
print(package.path)
-- 可通过环境变量 LUA_PATH 或代码追加
package.path = package.path .. ";./?/init.lua"
|
八、协程(Coroutine)
协程是一种比线程更轻量的协作式并发:同一时刻只有一个协程运行,由程序主动让出控制权。用它可以优雅地实现状态机、异步流程、迭代器。
| 函数 |
作用 |
coroutine.create(f) |
创建协程 |
coroutine.resume(co, ...) |
启动/恢复协程 |
coroutine.yield(...) |
让出协程 |
coroutine.status(co) |
查看状态(suspended/running/dead) |
coroutine.wrap(f) |
创建协程并返回直接可调用的函数 |
1
2
3
4
5
6
7
8
9
10
|
local co = coroutine.create(function(a, b)
print("开始", a, b)
local c = coroutine.yield(a + b) -- 让出,返回值给 resume
print("恢复,收到", c)
return "结束"
end)
print(coroutine.resume(co, 1, 2)) -- true 3
print(coroutine.resume(co, 10)) -- 恢复,收到 10 / true 结束
print(coroutine.resume(co)) -- false cannot resume dead coroutine
|
coroutine.wrap 更简洁,调用即 resume,错误会直接抛出:
1
2
3
4
5
6
7
8
9
|
local gen = coroutine.wrap(function()
for i = 1, 3 do
coroutine.yield(i)
end
end)
print(gen()) -- 1
print(gen()) -- 2
print(gen()) -- 3
|
游戏开发典型用途:把一段需要"分帧执行"的逻辑写成协程,每帧 resume 一次,避免卡顿。
1
2
3
4
5
6
7
8
9
10
11
|
local task = coroutine.wrap(function()
for i = 1, 3 do
print("执行第 " .. i .. " 步")
coroutine.yield()
end
end)
-- 每帧调用一次 task(),即可分帧完成
task() -- 执行第 1 步
task() -- 执行第 2 步
task() -- 执行第 3 步
|
九、错误处理
Lua 不强制捕获异常。错误可以显式抛出(error),也可以自动捕获并用 pcall / xpcall 处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
-- 主动抛错
local function divide(a, b)
if b == 0 then
error("除数不能为零", 2) -- 2 表示错误定位到调用者
end
return a / b
end
-- assert:条件为假时抛错,常用于参数校验
local function setAge(age)
assert(type(age) == "number", "age 必须是数字")
assert(age >= 0, "age 不能为负")
end
-- pcall:保护性调用,返回 (成功, 结果或错误信息)
local ok, result = pcall(divide, 10, 0)
print(ok, result) -- false 除数不能为零
local ok2, result2 = pcall(divide, 10, 2)
print(ok2, result2) -- true 5.0
-- xpcall:可传入错误处理函数,能获取堆栈信息
local function onError(err)
return "捕获错误:" .. debug.traceback(err, 2)
end
local ok3, msg = xpcall(function()
error("出错了")
end, onError)
print(ok3) -- false
print(msg) -- 捕获错误:...(含堆栈)
|
错误约定:抛出的错误对象通常是字符串;也可以用 error({ code = 500, msg = "..." }) 抛出 table,配合 pcall 结构化处理。
十、常用标准库
10.1 string 库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
local s = "Hello, Lua"
print(s:upper()) -- HELLO, LUA(用冒号调用方法)
print(s:lower()) -- hello, lua
print(s:len()) -- 10
print(string.sub(s, 1, 5)) -- Hello
print(string.rep("ab", 3)) -- ababab
print(string.reverse("abc")) -- cba
print(string.format("%d-%s-%.2f", 1, "x", 3.14159)) -- 1-x-3.14
-- 模式匹配(Lua 的"正则",功能较弱但够用)
print(string.find("hello world", "world")) -- 7 11
print(string.match("2026-09-19", "(%d+)-(%d+)-(%d+)")) -- 2026 9 19
print(string.gsub("a-b-c", "-", "+")) -- a+b+c 2
print(string.match("key=value", "(%w+)=(%w+)")) -- key value
-- 模式中的魔法字符
-- %d 数字 %a 字母 %w 字母数字 %s 空白 %p 标点
-- + 一次或多次 * 零次或多次 - 懒惰匹配 ? 可选
-- . 任意字符(除换行)
|
10.2 math 库
1
2
3
4
5
6
7
8
9
10
|
print(math.floor(3.7)) -- 3
print(math.ceil(3.2)) -- 4
print(math.abs(-5)) -- 5
print(math.max(1, 5, 3)) -- 5
print(math.min(1, 5, 3)) -- 1
print(math.sqrt(16)) -- 4.0
print(math.random(1, 100)) -- 随机整数
math.randomseed(os.time()) -- 播种,避免每次随机序列相同
print(math.huge, -math.huge, 0/0) -- 正无穷 负无穷 nan
|
10.3 os 库
1
2
3
4
5
|
print(os.time()) -- Unix 时间戳
print(os.date("%Y-%m-%d %H:%M:%S")) -- 格式化时间
print(os.clock()) -- CPU 时间(用于性能测试)
os.exit(0) -- 退出程序
-- os.execute("dir") -- 执行系统命令(注意安全)
|
10.4 io 库与文件读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-- 读文件
local file = io.open("data.txt", "r")
if file then
local content = file:read("*a") -- *a 读全部,*l 读一行
file:close()
print(content)
end
-- 写文件
local out = io.open("out.txt", "w")
out:write("Hello\n")
out:write("World\n")
out:close()
-- 按行遍历
for line in io.lines("data.txt") do
print(line)
end
|
游戏项目中文件 IO 常被沙盒限制,更多使用宿主提供的 IO 接口;读写配置文件时注意处理 io.open 返回 nil + 错误信息 的情况。
十一、面向对象
Lua 没有 class 关键字,但可以用表 + 元表实现类和对象。核心是 __index 实现方法和属性的继承。
11.1 类与实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
-- 定义"类"
local Animal = {}
Animal.__index = Animal -- 实例找不到的键,去 Animal 里找
-- 构造函数
function Animal.new(name, age)
local self = setmetatable({}, Animal) -- 新对象,元表是 Animal
self.name = name
self.age = age
return self
end
-- 实例方法(用冒号,隐式传入 self)
function Animal:speak()
print(self.name .. " 发出了声音")
end
function Animal:getName()
return self.name
end
-- 使用
local cat = Animal.new("咪咪", 2)
cat:speak() -- 咪咪 发出了声音
print(cat:getName()) -- 咪咪
-- 点号与冒号的等价关系
-- cat:speak() 等价于 cat.speak(cat)
|
11.2 继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
local Dog = {}
Dog.__index = Dog
setmetatable(Dog, { __index = Animal }) -- Dog 继承 Animal
function Dog.new(name, age, breed)
local self = Animal.new(name, age) -- 调用父类构造
setmetatable(self, Dog) -- 换成 Dog 的元表
self.breed = breed
return self
end
-- 重写父类方法
function Dog:speak()
print(self.name .. " 汪汪叫")
end
-- 调用父类方法
function Dog:describe()
Animal.speak(self) -- 显式调用父类实现
print("品种:" .. self.breed)
end
local dog = Dog.new("旺财", 3, "柴犬")
dog:speak() -- 旺财 汪汪叫(多态)
dog:getName() -- 继承自 Animal
print(dog:describe and "ok" or "no")
dog:describe() -- 旺财 发出了声音 / 品种:柴犬
|
11.3 封装(私有成员)
用闭包把变量藏在局部作用域中,实现真正的私有。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
local function newCounter()
local count = 0 -- 私有,外部无法直接访问
local obj = {}
function obj.inc()
count = count + 1
return count
end
function obj.get()
return count
end
return obj
end
local c = newCounter()
c.inc()
c.inc()
print(c.get()) -- 2
print(c.count) -- nil(外部访问不到)
|
十二、Lua 与 C / Unity
12.1 C 与 Lua 的交互
Lua 作为嵌入式语言,依赖 C API 与宿主通信,数据通过一个虚拟栈交换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
lua_State* L = luaL_newstate();
luaL_openlibs(L); /* 打开标准库 */
/* 执行 Lua 代码 */
luaL_dostring(L, "print('来自 C 的调用')");
/* 把一个 C 函数注册给 Lua */
lua_register(L, "c_add", c_add);
lua_close(L);
|
12.2 Unity 中的 Lua(xLua / ToLua)
Unity 游戏常用 xLua 或 ToLua 实现热更新:C# 侧提供接口,Lua 侧编写业务逻辑。
1
2
3
4
5
6
7
8
|
-- xLua 中调用 C# 示例
local GameObject = CS.UnityEngine.GameObject
local go = GameObject("MyObject")
go:AddComponent(typeof(CS.UnityEngine.Rigidbody))
-- 访问 C# 静态字段/方法
CS.UnityEngine.Time.deltaTime
CS.UnityEngine.Debug.Log("Hello from Lua")
|
1
2
3
4
5
|
// C# 侧加载并调用 Lua
LuaEnv env = new LuaEnv();
env.DoString("print('来自 C# 的 Lua 代码')");
env.DoString("require('game_logic')");
env.Dispose();
|
为什么游戏用 Lua 热更新:
- 逻辑改动无需重新编译 IL2CPP/C++,替换
.lua 文本即可
- 分包体积小,适合在线更新
- 脚本语言开发迭代快
注意:Lua 与 C# 之间跨语言调用有开销,高频逻辑(如每帧 Update)应尽量写在 C# 侧,Lua 只处理易变的业务逻辑。
十三、常见陷阱与最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
-- 1. 数组下标从 1 开始,不是 0
local t = { 10, 20, 30 }
-- t[0] 是 nil,不是 10
-- 2. 全局变量要避免,始终用 local
local function bad() x = 1 end -- 意外创建全局
local function good() local x = 1 end
-- 3. 0 和 "" 在条件判断中为真
if 0 then print("会执行") end
-- 判断空串要显式写:if s == "" then
-- 4. 表是引用类型,赋值不是拷贝
local a = { 1 }
local b = a
b[1] = 2
print(a[1]) -- 2
-- 5. 循环中拼接字符串性能差,用 table.concat
-- 差:
-- local s = ""
-- for i = 1, 10000 do s = s .. i end
-- 好:
local buf = {}
for i = 1, 10000 do buf[#buf + 1] = i end
local s = table.concat(buf)
-- 6. 用 == nil 判断存在,不要用 not
-- not 会把 false 也当成不存在
local flag = false
-- if not flag then ... end -- 误判
if flag == nil then end -- 正确
-- 7. pairs 遍历顺序不确定,不要依赖顺序
-- 需要顺序时先收集键再 table.sort
-- 8. # 的长度只在序列(从 1 连续)时可靠
local hole = { 1, 2, nil, 4 }
-- #hole 结果不确定(可能是 4,也可能是 2)
-- 9. 函数调用缺少 self 时用点号即可,有 self 用冒号
local obj = {}
function obj:method() end -- 定义用冒号
obj:method() -- 调用用冒号
-- 10. 深拷贝要递归,简单赋值只拷贝引用
|
最佳实践汇总:
- 一律用
local,减少全局污染
- 用
pcall 隔离不可信代码(如玩家脚本、热更脚本)
- 大表复用,避免频繁创建造成 GC 压力
- 用
table.concat 拼接大量字符串
- 模块返回一个表,私有函数放在
local 中
- 性能敏感处避免元表
__index 的深层链式查找
十四、高频面试问答
14.1 Lua 的数据类型有哪些?
8 种:nil、boolean、number、string、table、function、userdata、thread。其中 table 是唯一的数据结构,userdata 用于表示 C 侧对象,thread 表示协程。
14.2 为什么 Lua 数组下标从 1 开始?
历史与设计选择:更贴近数学和人类习惯(第一个元素是"第 1 个"),也便于用 # 表示长度、用 1..#t 遍历。代价是从 C 系语言转过来需要适应。
14.3 ipairs 和 pairs 的区别?
|
ipairs |
pairs |
| 遍历范围 |
数组部分(连续整数键) |
所有键值对 |
| 顺序 |
从 1 开始递增,遇 nil 停止 |
无序 |
| 底层 |
__index(5.1)/ 专用迭代器 |
next |
| 场景 |
遍历列表 |
遍历字典 |
14.4 __index 和 __newindex 的作用?
__index:访问不存在的键时触发,可用于默认值、继承、代理
__newindex:对不存在的键赋值时触发,可用于只读表、数据同步、拦截写入
- 二者都可以是表或函数,是 Lua 面向对象和元编程的核心
14.5 如何实现面向对象?
用表表示对象,用 __index 让实例查找到类的方法,用 setmetatable 建立类与父类的继承链。构造函数(new)中 setmetatable(self, Class),方法用 : 定义以隐式接收 self。
14.6 闭包是什么?有什么用?
闭包是函数加上它捕获的外部局部变量。用途:状态保持(计数器)、数据私有化(封装)、回调、迭代器、函数工厂。闭包捕获的是变量的引用而非拷贝。
14.7 协程和线程的区别?
协程是协作式的用户态并发,切换由程序显式 yield 控制,无抢占、无系统调度开销;线程是抢占式的系统级并发,可并行但需处理锁与竞态。Lua 协程本质是单线程内的控制流切换,适合状态机和异步流程。
14.8 pcall 和 xpcall 的区别?
都用于保护性调用并捕获错误。pcall 只返回成功标志与结果/错误信息;xpcall 额外接收一个错误处理函数,可以在其中调用 debug.traceback 获取堆栈,便于调试。
14.9 如何判断表中是否有某个键?
注意 t[k] ~= nil 会把值为 false 的情况也漏掉。正确做法是 t[k] == nil 判断不存在,或 5.2+ 用 rawget(t, k) ~= nil(绕过元表)。若确实可能存在 false 值,需用 next 或额外记录键集合。
14.10 Lua 的 GC 机制?
Lua 使用**增量式标记清除(mark-and-sweep)**垃圾回收器(5.4 引入分代模式)。GC 从根(全局表、栈、注册表)出发标记可达对象,未被标记的回收。可通过 collectgarbage() 手动控制,collectgarbage("count") 查看内存占用。
14.11 rawget / rawset 有什么用?
绕过元表的 __index / __newindex 直接读写表。用于实现元表逻辑时避免递归调用自身,或访问"真实"数据。
14.12 深拷贝和浅拷贝的区别?
浅拷贝只复制第一层引用(表还指向同一张子表),深拷贝递归复制所有层级。Lua 赋值运算符 = 永远是引用赋值,需要拷贝时必须手写递归函数。
本文系统梳理了 Lua 从基础语法到元表、协程、面向对象及工程实践的核心内容,适合零基础入门与游戏开发方向快速查阅。示例以 Lua 5.4 为准,涉及 5.1/LuaJIT 差异处已单独标注。