Featured image of post Lua入门

Lua入门

Lua 语言系统性入门,涵盖基础语法、Table、元表、闭包、协程、面向对象及与 C/Unity 的集成

一、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
lua -v        # Lua 5.4.x

运行代码的几种方式:

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 / falselocal b = true
number数字(5.3+ 区分整数/浮点,5.1 只有 double)10、3.14
string字符串(不可变)"hello"、'hi'
table表,唯一的数据结构{1, 2, 3}
function函数function f() end
userdataC 侧数据(如 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

六、元表与元方法(Metatable)

元表允许我们改变表的行为——定义表的加法、比较、索引、调用等操作。这是 Lua 面向对象和高级用法的基础。

6.1 setmetatable 与 getmetatable

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比较运算
__tostringtostring() 或 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 的区别?

ipairspairs
遍历范围数组部分(连续整数键)所有键值对
顺序从 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 差异处已单独标注。

不嘻嘻
使用 Hugo 构建
主题 Stack 由 Jimmy 设计