目录
一、可变参数机制
- 使用
...
语法声明可变参数,自动将传入参数转换为参数表。
示例:
function Print(...)
local tb = {...}
for k,v in pairs(tb) do
print(k,v)
end
end
Print(1,"哈吉米",2.5,"骰子",-10000);
运行结果:
二、参数与返回值的动态匹配
在lua中函数的传参、返回值的接收可以是个数不匹配的。
传入的参数与函数参数不匹配时,不会报错,按顺序接收,少的补nil,多的丢弃。
示例:
--接收两个参数
function F1(a)
print(a)
end
--不传入参数
F1()
--传入一个参数
F1("骰子")
--传入两个参数
F1(5,"骰子")
运行结果:
由于这个特性,Lua无法根据传入参数来确定调用哪个函数,也就不支持函数重载了。
返回值也是同理,多的返回值会丢弃,少的返回值补nil
function F2(a,b)
return a,b
end
--一个接收值接收
temp1 = F2(1,2)
print(temp1)
--两个接收值接收
temp1,temp2 = F2(1,2)
print(temp1,temp2)
--三个接受值接收
temp1,temp2,temp3=F2(1,2)
print(temp1,temp2,temp3)
运行结果:
这两个特性可以组合在一起使用,不会发生冲突。
三、 Lua中的闭包
Lua中的闭包往往发生在函数的嵌套中,一般是内层函数调用了外层函数中的局部变量,导致外层函数局部变量生命周期改变,从而形成闭包。
示例:
function outer()
local cnt = 0
return function()
cnt = cnt +1
print(cnt)
end
end
Inner = outer()
Inner()
Inner()
Inner()
运行结果:
闭包的特性:
特性 | 说明 | 内存表现 |
---|---|---|
变量捕获 | 捕获外层局部变量 | 创建独立upvalue域 |
生命周期延长 | 外层变量不随函数退出释放 | 引用计数机制维持 |
状态隔离 | 不同闭包实例独立维护状态 | 各自维护upvalue副本 |
为体现闭包特性,我们可以做个测试
function outer()
local cnt = 0
return function()
cnt = cnt +1
print(cnt)
end
end
Inner1 = outer()
Inner1()
Inner1()
Inner2 =outer()
Inner2()
Inner2()
运行结果:
这也就说明了不同闭包之间,各自维护upvalue副本。
四、 table
4.1 table的长度
在打印长度的时候,nil会被忽略,使用#来取table,往往是不准确的。
示例:
local list = {1,2,4,5,nil,nil,"骰子",6,nil}
print(#list)
此时代码的运行结果为4,而我们的table长度可不是4。
如何准确的记录table长度?
手动维护长度:
local list = {}
local list_len = 0
-- 插入操作
function list:add(v)
table.insert(self, v)
list_len = list_len + 1
end
-- 删除操作
function list:remove(index)
if self[index] then
table.remove(self, index)
list_len = list_len - 1
end
end
list:add(1)
list:add(12)
list:add(nil)
list:remove(2)
print(list_len) -- 实时准确长度
当然也可以通过下面的pairs去跑一遍遍历进行计数。
4.2 自定义索引
示例:
-- 基础数字索引
local t1 = {[1]="A", [2]="B", [3]="C"}
print(t1[1]) --> A
-- 字符串索引
local t2 = {name="骰子", ["age"]=10086}
print(t2["name"]) --> 骰子
print(t2.age)-->10086
--字符索引可以通过[""]也可以通过.的形式得到
-- 混合索引
local t3 = {
[0] = "零号元素",
["2025"] = "未来数据",
data = {["x"]=1,y=2}
}
print(t3["data"]["x"])-->1
自定义索引可能会遇到更大的坑。
示例:
local t1={[1]=1,[2]=2,[4]=4,[5]=5}
print(#t1)--这里输出的居然是5
local t2={[1]=1,[2]=2,[5]=5}
print(#t2)--这里又打印2
local t3 = {[1]=1,[2]=2,[-1]=-1,[3]=3,[0]=0,[4]=4}
print(#t3)--这里打印4
4.3 pairs与ipairs的区别
区别 | ipairs | pairs |
---|---|---|
遍历范围 | 连续数字索引(1到索引中断位置) | 所有非nil键 |
遍历顺序 | 严格1,2,3...顺序 | 哈希表存储顺序(不保证) |
性能表现 | O(n) 时间复杂度 | O(n) 但实际慢2-3倍 |
内存消耗 | 无额外消耗 | 需构建完整迭代状态机 |
典型应用 | 纯数组遍历 | 字典遍历/稀疏数组处理 |
示例:
local test_table = {
[1] = "A",
[2] = "B",
[4] = "D",
name = "骰子",
[3] = nil
}
print("ipairs遍历结果:")
for i,v in ipairs(test_table) do
print(i,v)
end
print("pairs遍历结果:")
for k,v in pairs(test_table) do
print(k,v)
end
运行结果:
4.4 函数冒号与点的区别
点是正常的调用和声明方法。
冒号调用方法 会默认把调用者 作为第一个参数传入方法中
冒号声明方法 会意味着会有默认传入的第一个参数 在函数内部可以同步self关键字拿到默认传入的第一个参数。
示例:
Position = {
x=1,
y=1,
z=1,
Print=function()
print(Position.x,Position.y,Position.z)
end
}
Position.Print()
--当然也可以这么写
function Position:Print()
print(self.x,self.y,self.z)
end
Position:Print()
Position.Print(Position)
4.5 元表
4.5.1 什么是元表?
在 Lua 里,元表是一种强大且核心的特性,它能助力我们动态地改变表的行为,帮助 Lua 数据变量完成某些非预定义功能的个性化行为。
4.5.2 设置元表
可通过setmetatable来设置元表
示例:
local myTable = {}
local metaTable = {}
setmetatable(myTable, metaTable) -- 将 metaTable 设置为 myTable 的元表
4.5.3 元方法
__index
当访问不到table表中的指定键时,如果该table有元表,lua会去找它元表中的__index键。
__index键对应为表时,会在该表格中去寻找该键,示例如下:
meta = setmetatable({},{
__index={
[1]=1,
[2]=2,
["骰子"]=3
}
})
myTable = setmetatable({},{
__index=meta
})
print(myTable["骰子"])
结果输出为3,这也说明了__index元方法是可以不断向上查找的。
__index键对应为方法时,lua会调用该方法,如果方法有返回值则结果为返回值,没有则结果为nil,示例如下:
tb=setmetatable({},{
__index=function (myTable,key)
if key == 1 then
return 1
else
print(key.."不存在")
return nil
end
end
}
)
print(tb[1])--输出1
print(tb[2])--输出 "2不存在" nil
__newindex
当给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法,如果存在则不直接进行赋值操作。
当__newindex对应为表时,会为其对应的表添加新键,示例如下:
mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })
print(mytable.key1)
mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey) --nil 新值2
mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)--新值1 nil
当__newindex对应为方法时,会调用对应方法,示例如下:
mytable = setmetatable({key1 = "value1"}, {
__newindex = function(mytable, key, value)
rawset(mytable, key, "\""..value.."\"")
end
})
mytable.key1 = "new value"
mytable.key2 = 4
print(mytable.key1,mytable.key2)--new value "4"
关于rawget和rawset,它们是用于直接操作table读写的函数,它们会绕开元表中定义的元方法。
更多元方法请看:Lua 元表(Metatable) | 菜鸟教程
4.6 实现面向对象
主要是使用__index元方法简单模拟了一下类。
Object={}
--基类Object
--new函数模拟实例化
function Object:new()
local tb = setmetatable({},{
__index=self
})
return tb
end
--subClass函数模拟继承
function Object:subClass(className)
_G[className]=setmetatable({},{
__index=self
})
_G[className].base=self
end
--示例
Object:subClass("Person")
Person.age=0
function Person:GrowUp()
self.age=self.age+1
end
p1 = Person:new()
p1:GrowUp()
print(p1.age)-- 1
p2= Person:new()
p2:GrowUp()
print(p2.age)-- 1
Person:subClass("Student")
function Student:GrowUp()
--注意这里不能写self.base:GrowUp()
--那样会把Person当第一个参数传进去,改的也就是Person的数据了
self.base.GrowUp(self)
end
st1 = Student:new()
st1:GrowUp()
st1:GrowUp()
print(st1.age)-- 2
st2=Student:new()
print(st2.age)-- 0
五、多脚本执行
5.1变量作用域规则
在 Lua 中,变量作用域分为局部和全局两种。只有使用 local
关键字标识的变量才是局部变量,未使用 local
标识的变量则为全局变量。
5.2模块加载机制
Lua 通过 require
关键字来加载并执行指定的模块。当使用 require("脚本名")
加载一个脚本时,会发生以下几件事:
-
require
首先会检查package.loaded
表中是否已经存在该模块,如果存在则直接返回对应的值,不再重新加载执行;若不存在,会尝试根据package.path
搜索模块文件,找到后加载并执行该脚本,最后将结果存入package.loaded
表。 -
脚本中的全局变量会被记录到
_G
表中,_G
是一个保存了 Lua 所有全局函数和全局变量的表,这样方便在其他地方直接访问这些全局变量。
5.3加载结果与返回值
如果被加载的脚本没有返回值,package.loaded[" 脚本名"]
中存储的默认值为 true
;若脚本有返回值,则 package.loaded[" 脚本名"]
中存储的是脚本的返回值。
示例,有 "Test.lua" 和 "Test2.lua" 两个脚本:
--Test.lua
local a=1
b= 2
print(a)
--Test2.lua
a=10
c=5
return a
require("Test")
require("Test2")
print(package.loaded["Test"]) --输出true
print(package.loaded["Test2"]) --输出10
print(a,b,c)-- 10 2 5
5.4模块卸载
可以通过 package.loaded[" 脚本名"] = nil
的方式来卸载模块。不过需要注意的是,这种卸载方式不会清除 _G
表中对应的全局变量。也就是说,即使卸载了模块,之前加载的全局变量仍然可以被访问。
示例:
require("Test") --输出 1
require("Test") --已经被加载过 不再执行脚本
print(b) -- 输出 2
package.loaded["Test"] =nil
print(package.loaded["Test"])
print(b) -- 置空loaded不会清空_G缓存,所以继续输出 2
require("Test") --输出 1
六、协程
6.1Lua中协程的基本概念
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。协同程序可以理解为一种特殊的线程,可以暂停和恢复其执行,从而允许非抢占式的多任务处理。
6.2协程与线程区别
线程和协同程序有以下主要区别:
-
调度方式:线程由操作系统抢占式调度,协同程序由程序员显式控制执行权转移。
-
并发性:线程并发执行,可多核心同时运行或单核心时间片轮转;协同程序协作式执行,同一时间仅一个运行,需主动放弃执行权。
-
内存占用:线程创建销毁有额外开销,因需独立堆栈和上下文;协同程序可共享堆栈和上下文,开销小。
-
数据共享:线程可共享内存,要注意安全和同步;协同程序通过参数和返回值共享数据,数据隔离性好。
-
调试和错误处理:线程较复杂,多线程交互和并发易致调试难题;协同程序相对简单,执行流程由程序员显式控制。
6.3Lua中协程的基本操作
本部分内容参考自Lua 协同程序(coroutine) | 菜鸟教程,原文教程非常详细,大家可以去看看。
协同程序由 coroutine 模块提供支持。
使用协同程序,你可以在函数中使用 coroutine.create 创建一个新的协同程序对象,并使用 coroutine.resume 启动它的执行。协同程序可以通过调用 coroutine.yield 来主动暂停自己的执行,并将控制权交还给调用者。
基本语法:
协程拥有四种状态,分别为挂起(suspended)、运行(running)、正常(normal)和死亡(dead),不同状态反映了协程在其生命周期中的不同阶段。
这里主要说一下正常状态:
当协程处于活跃状态,但没有被运行时,它处于正常状态。这意味着程序正在运行另一个协程,例如从协程 A 中唤醒协程 B,此时 A 处于正常状态,因为当前运行的是协程 B。在这种情况下,协程 A 虽然暂时没有在执行代码,但它仍然是活动的,并且可能在后续某个时刻继续执行。
基本操作示例:
function foo()
print("协同程序 foo 开始执行")
local value = coroutine.yield("暂停 foo 的执行")--这里value是用来接收传入参数的
print("协同程序 foo 恢复执行,传入的值为: " .. tostring(value))
print("协同程序 foo 结束执行")
end
-- 创建协同程序
local co = coroutine.create(foo)
-- 启动协同程序
--两个返回值,第一个是是否启动resume成功、第二个是yield()中返回的结果
local status, result = coroutine.resume(co)
print(result) -- 输出: 暂停 foo 的执行
-- 恢复协同程序的执行,并传入一个值
status, result = coroutine.resume(co, 42)
print(result) -- 输出: 协同程序 foo 恢复执行,传入的值为: 42
6.Lua协程与Unity协程的区别
Unity协程:基于C#的迭代器(IEnumerator
)和yield return
语法实现,通过Unity引擎的生命周期(Update
后、LateUpdate
前判断条件)自动调度。本质是通过迭代器分步执行,依赖Unity主线程的单线程模型。
Lua协程:基于coroutine
库(如create
、yield
、resume
)手动管理,完全由程序员控制切换时机。
两者核心差异在于调度机制(自动 vs 手动)和执行环境(引擎生命周期 vs 用户态逻辑)。Unity协程更适合与引擎深度集成的任务,而Lua协程更适合需要灵活控制状态的业务逻辑。
七、垃圾回收
Lua垃圾回收采取三色标记法,来标记-回收垃圾。
三色标记法的运作逻辑:
-
颜色状态划分
-
白色:初始状态或待回收对象(未被标记)
-
灰色:已扫描但引用链未遍历完的对象(中间状态)
-
黑色:已确认存活且引用链遍历完成的对象
-
-
标记流程
- 初始状态:所有对象均为白色,代表尚未被标记。
- 标记过程:从根对象(如全局变量、调用栈中的变量等)开始遍历,将根对象标记为灰色。
- 遍历阶段:不断从灰色对象集合中取出对象,将其引用的所有白色对象标记为灰色,然后将该灰色对象标记为黑色。当灰色对象集合为空时,标记过程结束。
- 回收阶段:此时,白色对象即为垃圾对象(未被访问到的对象),可以被回收;黑色对象是程序正在使用的对象,予以保留。
Lua 5.1后启用增量标记-清除算法,通过三个阶段分解降低单次停顿时长。
- 分步标记
- 拆解标记过程:把完整的标记过程拆分成多个小步骤。
- 处理部分引用链:每次仅处理部分灰色对象的引用链。
- 交还主线程:每个小步骤完成后,将控制权交还给主线程,让其执行用户代码,从而减少对程序运行的影响。
- 增量清除
- 分批次释放:内存释放过程也分批次进行。
- 逐步释放白色对象:通过多轮微操作,逐步释放标记为白色的垃圾对象。
- 并发控制
- 写屏障技术:采用写屏障(write barrier)技术来跟踪对象引用的变更。
- 避免漏标:主要跟踪黑色对象到白色对象的引用变更情况,防止在增量回收过程中出现漏标垃圾对象的问题。
Lua的垃圾回收器提供了可设置参数,通过collectgarbage
函数可动态调整GC行为:
-- 设置回收间隔率(触发GC的内存增长阈值)
collectgarbage("setpause", 200) -- 内存达上次回收后2倍时触发
-- 调整步进倍率(单次增量工作量)
collectgarbage("setstepmul", 300) -- 每次处理3倍基础工作量
-- 手动控制回收节奏
collectgarbage("step", 200) -- 执行200KB内存量的回收工作
collectgarbage("restart") -- 强制启动新回收周期
API:collectgarbage([opt [, arg]])
,opt
和 arg
是可选参数,具体用法如下:
opt 参数值 | 作用 | arg 参数说明 |
---|---|---|
"collect" | 执行一次完整的垃圾回收循环,这是 collectgarbage 函数的默认行为 | 无 |
"stop" | 停止垃圾回收器的运行,后续不会自动进行垃圾回收 | 无 |
"restart" | 重新启动已经停止的垃圾回收器,使其继续自动进行垃圾回收 | 无 |
"count" | 返回当前 Lua 使用的内存总量(以 KB 为单位),返回值是一个浮点数 | 无 |
"step" | 执行垃圾回收器的一步增量操作,arg 控制这一步操作的“大小”,值越大,操作越“大” | 传入一个表示操作大小的数字 |
"setpause" | 设置垃圾收集器间歇率,该值控制着收集器需要在开启新的循环前要等待多久。增大这个值会减少收集器的积极性 | 传入一个百分数(内部表示会将其转换,如 100 表示 1) |
"setstepmul" | 设置垃圾收集器步进倍率,控制着收集器运作速度相对于内存分配速度的倍率。增大这个值会让收集器更积极,且增加每个增量步骤的长度 | 传入一个百分数(内部表示会将其转换,如 100 表示 1) |
Lua 5.4 引入了C#相似的分代垃圾收集(Generational Garbage Collection)机制。
分代的概念:
将对象分为不同的代(generation),主要分为新生代(young generation)和老年代(old generation)。新创建的对象会被分配到新生代中,而在多次垃圾回收后仍然存活的对象会被晋升到老年代。
工作流程:
-
新生代垃圾回收
-
当新对象不断创建,新生代的空间逐渐被填满时,会触发新生代的垃圾回收。这种回收操作相对频繁,但速度较快,因为它只检查新生代中的对象。
-
在新生代垃圾回收过程中,Lua 会标记并清除那些不再被引用的对象。对于仍然存活的对象,如果它们已经经历了一定次数的垃圾回收,就会被晋升到老年代。
-
-
老年代垃圾回收
-
老年代的垃圾回收相对不那么频繁,因为老年代中的对象通常生命周期较长。当老年代的空间不足或者达到一定的触发条件时,会触发老年代的垃圾回收。
-
老年代垃圾回收的过程更为复杂,需要检查整个堆内存中的对象,包括新生代和老年代。它会标记所有仍然被引用的对象,然后清除那些未被标记的对象。
-
参考教程: