红色表示关键点 绿色表示额外注解
------------------------------------------------------------------------
一. 值与类型:
动态类型语言,类型存在于运行时,即变量无类型,值有类型。
库函数 type() 以字符串形式返回给定值的类型。
------------------------------ 类型一览 ------------------------------
nil(表示空,条件语句中同false)、
boolean、
number(64 位整数和双精度(64 位)浮点数)、
string(不可变的字节序列)、
function、
userdata(用户数据)、
thread (一个独立的执行序列,被用于实现协程)
table (是一个关联数组, 可以以除了 nil 和 NaN 之外的所有 Lua 值做索引。表是 Lua 中唯一的数据结构,可被用于表示普通数组、序列、符号表、集合、记录、图、树等等)。
---------------------------------------------------------------------------
二. 环境与全局环境:
_ENV: 每个被编译的 Lua 代码块都会生成一个外部局部变量 _ENV,它是一个上值(upvalue),作为
_ENV
的值的表都被称为环境。_ENV的默认值是_G(所以_G才能在全局任何地方被访问)。
_G:全局变量。Lua维护了一个“全局环境”,它的值被保存在 C 注册表的一个特别索引中。_G的初始值与这个全局环境的值相同。所有的标准库都被加载入全局环境。
三. 错误处理:
error() 用于显式地抛出一个错误。
pcall() 或 xcall() 用于捕获异常。
四. 元表及元方法:
Lua 中的每个值都可以有一个元表。 这个元表就是一个普通的 Lua 表, 它决定了原始值在特定操作下的行为(数学运算、位运算、比较、连接、 取长度、调用、索引等)(元表中定义了对各操作事件的处理方法)。 元表中还可以定义一个元方法__gc,当表对象或用户数据对象在垃圾回收时调用它。
任何值的元表。setmetatable() 用来设置/改变一张表的元表。
只可设置/改变表的元表(除非使用调试库)。
表和完全用户数据有独立的元表, 其它类型的值按类型共享元表,默认情况下,值没有元表,但字符串库在初始化的时候为字符串类型设置了元表。
元表可控制的操作的元方法,均以 " __ " 为前缀。当两个操作数不能直接进行要做的操作时(如,想加法时,但其中一个或两个为非数字),Lua会依次检查它们两个值是否存在为对应的元方法。如果存在,则将这两个操作数传入元方法,结果作为操作的结果。如果不存在,则抛出一个错误。
------------------------------ 元方法及调用时机一览 ------------------------------
- "add": + (加)操作。 "sub":
-
(减)操作。 "mul":*
(乘以)操作。 "div":/
(除以)操作。 "mod":%
(取余)操作。 "pow":^
(次方)操作。 "unm":-
(取负)操作。 "idiv"://
(向下取整除法)操作。 任一操作数不是number(能转换为数字的字符串除外)时事件被触发。 - "band":
&
(按位与)操作。"bor":|
(按位或)操作。 "bxor":~
(按位异或)操作。"bnot":~
(按位非)操作。 "shl":<<
(左移)操作。"shr":>>
(右移)操作。任一操作数无法转换为整数时事件被触发。 - "concat":
..
(连接)操作。任一操作数既不是字符串也不是数字时事件被触发。 - "len":
#
(取长度)操作。 操作数不是字符串时事件被触发。如果操作数是一张表且没有元方法, Lua 使用表的取长度操作 - "eq":
==
(等于)操作。 两个操作数都是表或都是完全用户数据且它们不是同一个对象时事件被触发。 - "lt":
<
(小于)操作。 两个操作数不全为整数也不全为字符串时事件被触发。 - "le":
<=
(小于等于)操作。 两个操作数不全为整数也不全为字符串时事件被触发。和其它操作不同, 此操作可能用到两个不同的事件。 首先,Lua 在两个操作数中查找 "__le
" 元方法。 如果一个元方法都找不到,就会再次查找 "__lt
" 元方法, 它会假设a <= b
等价于not (b < a)
。 调用的结果总会被转换为布尔量。 - "index": 索引
table[key]
。 当table
不是表或是表table
中不存在key
这个键时事件被触发。 __index可以是一个方法也可以是一张表,是函数时,table
和key
将以参数传入;是一张表时,则用key
对这个表取索引。 - "newindex": 索引赋值
table[key] = value
。 当table
不是表或是表table
中不存在key
这个键时事件被触发。__newindex可以是一个方法也可以是一张表。是函数时,table
、key 和 value
将以参数传入;是一张表时,对这张表做索引赋值操作。注意,这里是对这张表赋值,而不是对原来的table表赋值。(如果有必要,在元方法内部可以调用 rawset 来做赋值。) - "call": 函数调用操作
func(args)
。 当调用一个非函数的值的时事件被触发 (即func
不是一个函数)。 查找func
的元方法__call, 如果存在,func
作为第一个参数传入,原来调用的参数(args
)后依次排在后面。
------------------------------------------------------------------------------------------
五. 垃圾收集
增量标记-扫描收集器 以此自动管理内存。 垃圾收集器间歇倍率 和 垃圾收集器步进倍率,控制着垃圾收集的循环。
垃圾收集器间歇倍率:控制着收集器开启新的循环前要等待多久(相邻两次循环的间隔)。数值为小于100时无间隔,数值为200时内存为上次 2 倍时执行一次(以百分数为单位)。collectgarbage("setpause", 99) 。
垃圾收集器步进倍率:控制着收集器运作速度相对于内存分配速度的倍率。数值为100时与内存分配速度相等,数值为200时为2倍的内存分配速度(以百分数为单位)。collectgarbage("setstepmul", 200)。
可以通过在 C 中调用 lua_gc() 或在 Lua 中调用 collectgarbage() 来改变这俩数字。 这两个函数也可直接控制收集器(例如停止它或重启它)。
在为一个对象设置元表时,在其中定义 "__gc
" 元方法,就标记了这个对象需要触发终结器。 终结器允许你配合 Lua 的垃圾收集器做一些额外的资源管理工作 (例如关闭文件、网络或数据库连接,或是释放一些你自己的内存)。NRatel个人认为其类似于C#中实现IDispose接口,对非托管内存的管理的机制。
终结器的触发次序为标记次序的逆序。 即,先标后调,后标先调,类似栈序。
先被回收,后又因为终结器用到的对象,会被lua复活,一般情况下是短暂复活(在下次垃圾收集循环释放),但如果在终结器中将对象存到了全局,则会永久复活。
unreachable, NRatel个人理解为:不可访问,无引用),它的终结函数还会再调用一次。 无论是哪种情况, 对象所属内存仅在垃圾收集循环中该对象不可达 且 没有被标记成需要触发终结器才会被释放。
当使用 lua_close() 关闭一个状态机时, Lua 将调用所有标记为终结的终结器。在这个过程中,任何终结器再次标记对象的行为都不会生效。
------------------------------------------------------------------------------------------
弱表 指内部元素为 弱引用 的表。可以是弱键-强值、强键-弱值、弱键-弱值。 垃圾收集器会忽略掉弱引用(即,按未引用处理)。
强键-弱值的表允许值的回收,但会阻止键的回收;弱键-弱值的表,收集器可以回收其中的任意键和值。
任何情况下,只要键或值的任意一项被回收, 相关联的键值对都会从表中移除。
通过元表中的__mode
字段(field)控制表中的弱属性。__mode
包含字符 'k
' 时,表中所有键都为弱引用,包含字符 'v
' 时表中所有值都为弱引用。
弱键-强值的表,称为暂时表。 暂时表中,只有当键可访问时,它的值才可被访问。特别注意,如果一个键只被它的值引用,那么这个键值对将被删除。
对一张表的弱属性的修改仅在下次收集循环才生效。 尤其是,当你把表由弱改强时,Lua 可能在修改生效前回收表内一些项目。
只有那些有显式构造的对象才会从弱表中移除。 某些如数字和轻量C函数的值,不受垃圾收集器管辖, 因此不会从弱表中移除 (除非它们关联的值被回收)。 虽然字符串受垃圾回收器管辖, 但它们没有显式的构造过程,所以也不会从弱表中移除。
弱表针对复活的对象 (指那些正在走终结流程,仅能被终结器访问的对象) 有着特殊的行为。 弱值引用的对象,在运行它们的终结器前就被移除了, 而弱键引用的对象则要等到终结器运行完毕后,到下次收集当对象真的被释放时才被移除。 这个行为使得终结器运行时得以访问到由该对象在弱表中所关联的属性。
如果一张弱表在当次收集循环内的复活对象中, 那么在下个循环前这张表有可能未被正确地清理。
六. 协程
协程有三个状态:suspended(挂起)、running(运行)、dead(函数走完后的状态,这时候不能再重新resume)。
coroutine.create() 创建协程。参数是协程主函数。 返回值是其句柄 (一个 thread 类型的对象)。创建后,协程处于挂起状态。
coroutine.resume() 执行协程,使协程从挂起变为运行。首个参数是 coroutine.create 创建所得的句柄,之后的参数是传递给协程函数的参数。返回值在正常结束时,返回true和协程主函数的返回值;在发生错误时,返回false和错误信息。
coroutine.yield() 使协程挂起,并可传入参数。 协程挂起时, coroutine.resume() 返回 true和传入的参数。 当下次重启此协程时, 会接着从挂起点继续执行。这时,之前的 coroutine.yield 会返回,返回值是传给重启协程的方法 coroutine.resume() 的第一个参数之外的其他参数。
coroutine.wrap() 也会创建一个协程。 不同的是,它返回一个函数。 启动协程直接调用这个函数即可,而不是再使用coroutine.resume 。传入参数相当于 coroutine.resume 的额外参数。 函数只返回协程主函数的返回值,不会捕获错误,不会返回是否正常结束的bool标志。
额外注意点:
1. 当一个可以完全表示为整数的浮点数做为键值时, 都会被转换为对应的整数储存。 例如,t[2.0]=1, 实际被插入表t中的键是整数 2,另一方面,2 与 "2" 是两个不同的 Lua 值, 故而它们可以是同一张表中的不同项。
2. 表、函数、线程、以及完全用户数据在 Lua 中被称为 对象: 变量并不真的 持有 它们的值,而仅保存了对这些对象的 引用,赋值、参数传递、函数返回,都是针对引用而不是针对值的操作, 这些操作均不会做任何形式的隐式拷贝。
3. 访问元表中的元方法永远不会触发另一次元方法。
4. 对于一元操作符(取负、求长度、位反), 元方法调用的时候,第二个参数是个哑元,其值等于第一个参数。这样处理仅仅是为了所有的操作都和二元操作一致。
5. 在为一个对象设置元表时,如果设置时没有 __gc
,之后才给元表加上 __gc, 那这个对象是没有被成功标记的。 即,__gc 必须在一开始设置元表时就定义进去,不能后来加入。不用担心的是,__gc
在一开始设置标记后,还是可以修改的。