Lua高级特性学习

目录

一、可变参数机制

二、参数与返回值的动态匹配

三、 Lua中的闭包

四、 table

4.1 table的长度

4.2 自定义索引

4.3 pairs与ipairs的区别

4.4 函数冒号与点的区别

4.5 元表

4.5.1 什么是元表?

4.5.2 设置元表

4.5.3 元方法

__index 

__newindex 

4.6 实现面向对象

五、多脚本执行

5.1变量作用域规则

5.2模块加载机制

5.3加载结果与返回值

5.4模块卸载

六、协程

6.1Lua中协程的基本概念

6.2协程与线程区别

6.3Lua中协程的基本操作

6.Lua协程与Unity协程的区别

七、垃圾回收


一、可变参数机制

  • 使用...语法声明可变参数,自动将传入参数转换为参数表。

示例:

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的区别
区别ipairspairs
遍历范围连续数字索引(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("脚本名") 加载一个脚本时,会发生以下几件事:

  1. require 首先会检查 package.loaded 表中是否已经存在该模块,如果存在则直接返回对应的值,不再重新加载执行;若不存在,会尝试根据 package.path 搜索模块文件,找到后加载并执行该脚本,最后将结果存入 package.loaded 表。

  2. 脚本中的全局变量会被记录到 _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协程与线程区别

线程和协同程序有以下主要区别:

  1. 调度方式:线程由操作系统抢占式调度,协同程序由程序员显式控制执行权转移。

  2. 并发性:线程并发执行,可多核心同时运行或单核心时间片轮转;协同程序协作式执行,同一时间仅一个运行,需主动放弃执行权。

  3. 内存占用:线程创建销毁有额外开销,因需独立堆栈和上下文;协同程序可共享堆栈和上下文,开销小。

  4. 数据共享:线程可共享内存,要注意安全和同步;协同程序通过参数和返回值共享数据,数据隔离性好。

  5. 调试和错误处理:线程较复杂,多线程交互和并发易致调试难题;协同程序相对简单,执行流程由程序员显式控制。

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库(如createyieldresume)手动管理,完全由程序员控制切换时机。

两者核心差异在于调度机制(自动 vs 手动)和执行环境(引擎生命周期 vs 用户态逻辑)。Unity协程更适合与引擎深度集成的任务,而Lua协程更适合需要灵活控制状态的业务逻辑。

七、垃圾回收

Lua垃圾回收采取三色标记法,来标记-回收垃圾。

三色标记法的运作逻辑:

  1. 颜色状态划分

    • 白色:初始状态或待回收对象(未被标记)

    • 灰色:已扫描但引用链未遍历完的对象(中间状态)

    • 黑色:已确认存活且引用链遍历完成的对象

  2. 标记流程

    • 初始状态:所有对象均为白色,代表尚未被标记。
    • 标记过程:从根对象(如全局变量、调用栈中的变量等)开始遍历,将根对象标记为灰色。
    • 遍历阶段:不断从灰色对象集合中取出对象,将其引用的所有白色对象标记为灰色,然后将该灰色对象标记为黑色。当灰色对象集合为空时,标记过程结束。
    • 回收阶段:此时,白色对象即为垃圾对象(未被访问到的对象),可以被回收;黑色对象是程序正在使用的对象,予以保留。

Lua 5.1后启用增量标记-清除算法,通过三个阶段分解降低单次停顿时长。

  1. 分步标记
    • 拆解标记过程:把完整的标记过程拆分成多个小步骤。
    • 处理部分引用链:每次仅处理部分灰色对象的引用链。
    • 交还主线程:每个小步骤完成后,将控制权交还给主线程,让其执行用户代码,从而减少对程序运行的影响。
  2. 增量清除
    • 分批次释放:内存释放过程也分批次进行。
    • 逐步释放白色对象:通过多轮微操作,逐步释放标记为白色的垃圾对象。
  3. 并发控制
    • 写屏障技术:采用写屏障(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 会标记并清除那些不再被引用的对象。对于仍然存活的对象,如果它们已经经历了一定次数的垃圾回收,就会被晋升到老年代。

  • 老年代垃圾回收

    • 老年代的垃圾回收相对不那么频繁,因为老年代中的对象通常生命周期较长。当老年代的空间不足或者达到一定的触发条件时,会触发老年代的垃圾回收。

    • 老年代垃圾回收的过程更为复杂,需要检查整个堆内存中的对象,包括新生代和老年代。它会标记所有仍然被引用的对象,然后清除那些未被标记的对象。

参考教程:

Lua 元表(Metatable) | 菜鸟教程

Lua 协同程序(coroutine) | 菜鸟教程

Lua 垃圾回收 | 菜鸟教程

Lua面向对象实践-CSDN博客

Lua 快速入门(四)——多脚本执行_多个lua文件怎么执行-CSDN博客

Lua内存管理与垃圾收集机制详解-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值