文章目录
零、相关文章
【Lua】 Lua学习笔记(一)
【Lua】 Lua学习笔记(二)
【Lua】 Lua学习笔记(三)
【Lua】 Lua学习笔记(四)
【Lua】 Lua学习笔记(五)
【Lua】 Lua学习笔记(六)
一、前言
哈喽大家好,我是 FEZ98 ,今天继续系统学习Lua。这个系列是我系统学习Lua语言的学习笔记,我会把遇到的一些比较值得记录与关注的知识写在里面,供自己以后进行回顾。
二、Lua 元表(Metatable)
在 Lua table 中可以访问对应的 key 获取 value 值,但是无法对两个 table 进行操作(比如相加、相减)。
因此 Lua 提供了 Metatable(元表)
,允许我们改变 table 的行为,每个行为关联了对应的元方法。例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。
当 Lua 试图对两个表进行相加操作时,先检查两者之一是否有元表
,之后检查是否有一个叫 _add
的字段,若找到,则调用对应的值。_add 等即时字段,其对应的值(往往是一个函数
或是table
)就是“元方法”。
(2.1)setmetatable() 和 getmetatable()
有两个很重要的函数来处理元表:
setmetatable(table,metatable)
: 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table)
: 返回对象的元表(metatable)。
实例:
(2.2)__index 元方法
这是 metatable 最常用的键。(注意:__index 是两个_)
当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index
键。如果 __index
包含一个表格,Lua会在表格中查找相应的键。
实例1(__index)
:
如果 __index 包含一个函数的话,Lua 就会调用那个函数,table 和键会作为参数传递给函数。
__index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。
实例2(__index 为函数)
:
实例解析:
- mytable 表赋值为 {hobby= “Coding”}。
- mytable 设置了元表,元方法为 __index。
- 在 mytable 表中查找 hobby,如果找到,返回该元素,找不到则继续。
- 在mytable表中查找 like,如果找到,返回该元素,找不到则继续。
- 判断元表有没有 __index 方法,如果 __index 方法是一个函数,则调用该函数,函数的参数为 mytable 和 key。
- 元方法中查看是否传入 “like” 键的参数(mytable.like已设置),如果传入 “like” 参数返回 “Swimming”,否则返回 nil 。
- mytable.null 的查找步骤同上述步骤4 至 步骤6。
可以将代码简化为如下:
总结:
- 在表中查找,如果找到,返回该元素,找不到则继续
- 判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
- 判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。
(2.3)__newindex 元方法
__newindex
元方法用来对表更新
,__index
则用来对表访问
。
当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。
实例解析:
在以上实例设置 __newindex 元方法后,在对 mytable 中不存在的新索引键赋值时,会调用 __newindex方法,而不进行赋值操作。而如果对已经存在的索引键进行赋值时,则会进行赋值,不调用 __newindex 方法。
实例(使用rawset函数更新表)
(2.4) 两表操作
(2.4.1) _add
(2.4.2)__sub
(2.4.3)__eq
(2.4.4)其他操作符
表中对应的操作列表如下:(注意:__
是两个下划线)
(2.5)__call 元方法
__call 元方法在 Lua 调用一个值时调用。
实例(计算两表元素之和)
(2.6)__tostring 元方法
__tostring 元方法用于修改表的输出行为。
实例(计算表中元素之和)
三、Lua 协同程序(coroutine)
Lua 中的协同程序(coroutine)与线程类似:拥有独立的堆栈
,独立的局部变量
,独立的指令指针
,同时又与其他协同程序共享全局变量
和其他大部分东西。
协程可以理解为一种特殊的线程,可以暂停与恢复其执行,从而允许非抢占式的多任务处理。
(3.1)基本语法
协同程序由 coroutine
模块提供支持。
使用协同程序,可以在函数中使用 coroutine.create
创建一个新的协同程序对象,并使用 coroutine.resume
启动它的执行。协同程序可以通过调用 coroutine.yield
来主动暂停自己的执行,并将控制权交还给调用者。
实例1
实例解析:
- 首先创建了一个
myCoro
的函数作为协同程序,在函数中使用了coroutine.yield
将当前协程挂起,并将"协程挂起"
字符串作为参数赋值给value
作为返回值。- 之后在主程序中使用
coroutine.create
创建了一个协程,使用coroutine.resume
执行当前协程。- 当协程运行到
coroutine.yield
时,当前运行的协程暂停,并返回了一个值给调用者。- 当再次调用
coroutine.resume
时,传入一个值作为协同程序恢复执行时的参数,协程继续执行,并打印传入的数据。
协同程序的状态可以通过 coroutine.status
函数获取,通过检查状态可以确定协同程序的执行情况(如运行中
、已挂起
、已结束
等)。
实例(包含上表中的所有用法
)
从 coroutine.running
就可以看出来,coroutine
在底层实现就是一个线程。
当create
一个 coroutine
的时候就是在新线程中注册了一个事件。
当使用 resume
触发事件的时候,create
的 coroutine
函数就被执行了,当遇到 yield
的时候就代表挂起当前线程,等候再次resume
触发事件。
实例:
实例解析:
调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;yield 除了挂起协程外,还可以同时返回数据给 resume ,并且还可以同时定义下一次唤醒时需要传递的参数。
- 第一次调用(第一次调用的时候,协同程序是一个挂起的状态),resume 的参数 1,2传入主体函数,打印得出 1,2,之后调用
myMul
打印得出myMul 函数输出
,程序挂起,之后返回这个值到 resume,作为输出值4
。- 第二次调用 resume 参数为
r
,从主函数中print("coroutine second output: ",result1)
开始运行,因为此时的状态是挂起的状态,resume 的参数传入 yield,作为挂起点的返回值为result1
。
所以打印得出 r ,之后继续运行,执行local result2,result3=coroutine.yield(x+y,x-y)
,因为此时,并非 resume 直接调用的情况,所以 yield 函数 使用主函数传入的 1,2 参数 作为参数,得出结果为 3, -1,之后 再次挂起。
(注意如果此时 local result1=myMul(x+1) 变为 local result1,resultA,resultB=myMul(x+1)):
[1]resume传入参数少于接收参数 (local result1,resultA,resultB):
[2] resume传入参数等于接收参数 (local result1,resultA,resultB):
[3] resume传入参数大于接收参数 (local result1,resultA,resultB):
- 第三次调用 resume 参数为 x, y ,从程序挂起点运行,并参数传入 yield 中,yield 此时作为返回值点,所以得出 result2,result3 结果为
x
,y
,之后继续运行 return b, “coroutine fourth output” , 返回 y, 为2
。- 总结: resume 执行的情况如果(排除第一次执行情况)是挂起的状态,那么 resume 的参数传递给 yield,yield 不论参数表达式形式,返回的值 resume 传递的所有参数。
特别的,注意如果运行之后,再次挂起,那么此时传入的 yield 值,就是主函数的参数值,如果使用的话。
如果 resume 的执行是第一次(上面讲到排除第一次挂起的特殊情况)的情况或者是挂起之后再次运行,那么 resume 的参数 作为主函数的参数。
(3.2)生产者-消费者问题
使用 Lua 协程实现生产者-消费者问题:
(3.3)线程与协程的区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程
,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行
,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
主要区别归纳如下:
调度方式
:线程通常由操作系统的调度器进行抢占式调度,操作系统会在不同线程之间切换执行权。而协同程序是非抢占式调度的,它们由程序员显式地控制执行权的转移。
并发性
:线程是并发执行的,多个线程可以同时运行在多个处理器核心上,或者通过时间片轮转在单个核心上切换执行。协同程序则是协作式的,只有一个协同程序处于运行状态,其他协同程序必须等待当前协同程序主动放弃执行权。
内存占用
:线程通常需要独立的堆栈和上下文环境,因此线程的创建和销毁会带来额外的开销。而协同程序可以共享相同的堆栈和上下文,因此创建和销毁协同程序的开销较小。
数据共享
:线程之间可以共享内存空间,但需要注意线程安全性和同步问题。协同程序通常通过参数传递和返回值来进行数据共享,不同协同程序之间的数据隔离性较好。
调试和错误处理
:线程通常在调试和错误处理方面更复杂,因为多个线程之间的交互和并发执行可能导致难以调试的问题。协同程序则在调试和错误处理方面相对简单,因为它们是由程序员显式地控制执行流程的。
总体而言,线程
适用于需要并发执行
的场景,例如在多核处理器上利用并行性加快任务的执行速度。而协同程序
适用于需要协作和协调
的场景,例如状态机、事件驱动编程或协作式任务处理。选择使用线程还是协同程序取决于具体的应用需求和编程模型。