lua5.3程序设计精粹

1.在注释代码时我们可以使用如下方式:

--[[
代码段
--]]

这样当要还原注释块代码时只需要在第一行前面加上一个-就可以将第一行变成单行注释,而最后一行的–]]本身就是单行注释。如下所示:

---[[
代码段
--]]

2.lua中条件语句将除Boolean值false和nil外的所有其他值视为真,否则视为假。常见的逻辑运算中,and和or都遵循短路求值原则,即只在必要时才对第二个操作数进行求值。如下所示:
and的运算结果为:如果第一个操作数的结果为条件false,则返回第一个操作数,否则返回第二个操作数。
or运算结果为:如果第一个操作数的结果为条件true,则返回第一个操作数,否则返回第二个操作数。
not运算结果为:永远返回Boolean类型值。

3.具有十进制的小数或者指数的数值会被当做浮点型值,否则会被当做整形值。常见要点如下:
1>.不论浮点型还是整形值,使用type函数获取的类型都是number。
2>.浮点值使用math.type函数获取的值为float,整形值使用math.type获取的值为integer。
3>.浮点值得精度为双精度浮点值,整型值精度为64位长度整形值。如果想要缩短number类型长度,可以使用LUA_32BITS宏来限定浮点值为单精度浮点值,整形值为32位长度整形值。
4>.使用string.format("%a", 十进制数)来获取十六进制数。其中十六进制浮点数(小数部分和以p开头的指数部分组成)可以保留所有的浮点数精度,并且比十进制转换的速度更快。
5>.算数运算中,除了除法得到的结果永远为浮点值外,其他所有的运算操作均满足:操作数都是整形时,得到的运算结果为整形数值,否则得到的运算结果为浮点数值。
6>.十进制的有限小数在用二进制表示时可能是无限小数,从而造成运算不对。如:12.7-20+7.3的结果就不为0,原因就是12.7和7.3用二进制表示小数位是无限的。
7>.对整形数值进行越界处理时会发生回环(最大值变最小值,最小值变最大值);对浮点值进行越界处理时会得到一个近似值。
8>.浮点数值转换成整形数值时,必须满足整形数值的形式和长度要求。通常有以下方式:
.使用异或0(|0)来对浮点数值进行转换处理,转换失败会抛出异常。
.使用math.tointeger函数来对浮点数值进行转换处理,转换失败会返回nil。

4.字符串具有以下特性:
1>.单引号括起来的字符串中出现双引号可以不用转义,双引号括起来的字符串中出现单引号也可以不用转义。否则就都需要进行转义处理。
2>.可以使用[描述符[字符串内容]描述符]来表达长字符串或者多行字符串。其中描述符前后必须保持一致,且可以为空串。
3>.\z可以将后面的所有空格字符给过滤掉,直到遇到不为空格字符为止。
4>.数值字符串进行算数运算时得到的结果为浮点型数值;数值进行字符串操作时得到的结果为字符串。
5>.字符串是不可修改的,对字符串进行的所有操作而得到的字符串都是新建的。

5.表具有以下特性:
1>.当被用作表索引时,任何能够被转换为整形的浮点数都会被转换成整形数。如a[2.0] = 10等价于a[2] = 10。
2>.表在使用pairs遍历时是无序的,但是每个条目只可能被遍历一次。且表中key不能为nil,但是值可以为nil。当值为nil时表示不存在或者删除这个条目,也不会计数到表的长度中。
3>.对表成员连续访问时,为了降低访问次数,可以使用安全访问的方式进行操作。如下所示:

-- 在一次成功的访问中对表进行了6次访问,也就是例子中的6个访问符号"."
zip = company and company.director and company.director.address and company.director.address.zipcode
-- 基于类似安全访问的形式,一次成功访问对表进行3次访问,也就是例子中的3个访问符号"."
E = {}
zip = (((company or E).director or E).address or E).zipcode

4>.table中的move函数可以用来对表中指定区间数据移动到表中指定位置后面,也可以将表中指定区间数据移动到另一个表中指定位置后面。这些移动操作都是一个拷贝参数。移动的表数据长度并没有发生改变,也就是说移动后不要的位置数据可以自己置空处理。

6.函数具有以下特性:
1>.函数参数只有一个且为字符串或者表构造器时,可以不用写()。
2>.当函数内部返回多个值时,只有当函数调用是一系列表达式的最后(或者唯一)一个表达式时才能返回多个结果,否则只能返回一个结果,其他结果会被丢弃掉。
3>.对返回多个结果的函数使用()包起来可以强制其只返回一个结果。
4>.table.pack函数用来将参数列表以及参数个数保存在一个表中进行返回,其中参数个数用变量n存储;table.unpack函数用来交参数表按照从头到尾输出参数,直到遇到第一个nil或者遍历完参数表为止,也可以指定输出参数表中指定区间范围的参数。
5>.select函数用来将参数列表按照指定selector方式输出。当selector为“#”时,此时会输出参数个数;当selector为索引值时,此时会输出索引值后(包含该索引位置)的所有参数。
6>.尾调用是指在函数的最后一行调用其他函数后立马结束函数调用,也就是格式为:"return func(args)"的形式。其中尾调用函数不会占用栈空间大小。

7.输入和输出具有以下特性:
1>.io.input函数参数为空时表示获取当前输入流,参数为指定输入流时表示获取该指定输入流。
2>.io.output函数参数为空时表示获取当前输出流,参数为指定输出流时表示获取该指定输出流。
3>.io.read函数表示从当前输入流中进行读取数据。其中可以读取指定字节大小的数据,也可以读取所有内容或者一行内容等。
4>.io.write函数表示从当前输出流中进行写入数据。
5>.io.lines函数用来从当前输入流中读取所有行数据或者指定字节大小数据。
6>.io.open函数表示以只读或者只写或者二进制等形式来操作指定文件,并获取该文件句柄。通过该文件句柄调用read来读取文件内容,调用write来写入文件内容,调用seek来返回当前新位置在流中相对于文件开头的偏移,调用close来关闭该文件句柄。
7>.io.flush函数将所有缓冲区数据写入文件。
8>.io.popen函数用来执行执行系统命令,并重定向命令的输入输出。
9>.setvbuf函数用来设置缓冲区模式。当为no时表示无缓冲区;当为full时表示缓冲区满时才写入数据;当为line时表示当遇到换行时才写入数据;
10>.os.rename函数用于文件的重命名。os.remove函数用来删除文件。os.exit函数用于终止程序执行,参数可以为0或者true表示执行成功,也可以为非0或者false表示执行失败。os.getenv函数用来获取指定环境变量的配置信息。os.execute函数用来执行系统命令,且第一个返回值表示程序是否成功完成,第二个返回值表示程序完成状态,第三个返回值表示信号代码。

8.局部变量具有以下特性:
1>.局部变量只有在所在代码块中有效,且不能超过200个。由于交互模式(终端命令行,使用lua -i进入)中每一行语句就是一个单独的代码块,所以不能直接在交互模式下访问局部变量,此时可以使用"do 局部变量定义以及访问等操作 end"的模式来使用,因为解析器发现do后,直到出现end才会认为代码块结束。
2>.局部变量访问速度比全局变量快。
3>.局部变量使用完毕后可以被垃圾回收器进行回收。
4>.局部变量可以避免由于不必要的命名而造成全局变量的混乱。也可以避免同一程序中不同代码块中命令冲突。
5>.局部变量在使用时才进行定义和初始化,这样可以提高可读性。
6>.使用repeat until循环语句时,repeat代码块中定义的局部变量在until中也可以使用。

9.跳转命令具有以下特性:
1>.return只能是代码块的最后一句或者end,else和until之前的最后一句。否则会抛出系统异常。
2>.goto语句的格式为"goto 标签名字 “::标签名字:: 其他代码”",其中goto语句不能跳转到代码块内;不能跳转转到函数外;不能跳转到局部变量作用域处;可以使用goto来模拟continue和redo,状态机等操作。

10.函数具有以下特性:
1>函数都是匿名的,所谓的函数名实际上只是包含匿名函数的变量名而已。如下所示:

-- 全局函数定义:
function foo()  end  <=>  foo = function () end
--局部函数定义
local function goo() end <=> local goo; goo = function () end

2>.局部间接递归函数在定义时,最后一个函数不能加上local关键字,否则就会出现最后局部变量函数的前向声明变成未知状态,从而在使用时就报错。如下所示:

-- 最后一个局部函数的前向声明
local f

-- 局部函数g的定义
local function g()
	-- 调用局部函数f
	f()
end

-- 局部函数f的定义,此处不能加上local,否则上面的local f就会变成未知状态,函数g中访问f就会出问题
function f()
	-- 调用局部函数g
	g()
end

3>.函数是第一类值,也就是说函数可以作为其他函数的参数,也可以作为其他函数的返回值,还可以用来存储在表中或者全局和局部变量中。
4>.当函数B中调用函数A时,此时函数A可以访问函数B中所有的局部变量。而函数B中的局部变量在函数A中既不是局部变量也不是全局变量而是上值upvalue。这个上值只有在函数A调用完毕后才会回收掉。这种函数B调用函数A的情形就叫做闭包。

11.模式匹配具有以下特性:
1>.string.find函数具有四个参数。第一个参数表示原始字符串;第二个参数表示匹配模式;第三个参数表示起始查找位置;第四个参数表示是否使用简单搜索(第二个参数在第一个参数中只进行单纯的查找子字符串操作)。当查找成功时就会返回起始和终止位置,否则返回nil。
2>.string.match函数具有三个参数。第一个参数表示原始字符串;第二个参数表示匹配模式;第三个参数表示起始查找位置;当查找成功时就会返回查找到的字符串,否则返回nil。
3>.string.gsub函数具有四个参数。第一个参数表示原始字符串;第二个参数表示匹配模式;第三个参数表示替换字符串(也可以是表或者函数。其中表是以查找到的字符串作为key来获取替换字符串;而函数是将查找的字符串作为参数并返回替换字符串。当获取的替换字符串为nil时就不改变这个查找的字符串);第四个参数表示替换次数。当替换成功时返回替换后的字符串以及替换次数,否则返回原始字符串。
4>.string.gmatch函数具有两个参数。第一个参数表示原始字符串;第二个参数表示匹配模式。该函数会返回一个函数,然后返回的函数会遍历原始字符串中所有出现匹配模式的字符串。
5>.字符( ) . % + - * ? [ ] ^ $表示魔法字符。用法如下:
():表示从目标字符串中捕获满足()中指定的模式匹配的内容用于后续用途。可以使用%n且n为数字来表示获取第n个捕获结果的副本,当n为0时表示整个匹配。如果()中没有指定任何模式匹配的话,就表示获取()后面字符在目标字符串中的位置,当字符在()前面时就表示字符在目标字符串中的位置处的下一个位置。
.:表示任意字符。
%:表示转义字符。当%后面跟的是大写形式类就表示是小写形式类的补集。如:%a表示匹配所有字母字符,%A表示匹配任意非字母字符。同时这些魔法字符也可以使用%进行转义。如:%%就是匹配一个%;%?就是匹配一个?
+:表示重复一次或者多次。
-:表示区间范围。如:[0-7]表示字符0开始到字符7终止,可以表示匹配8进制数。也可以表示重复0次或者多次,且按照最短满足字符进行匹配。
*:表示重复0次或者多次。按照最长满足字符进行匹配。
?:表示可选(出现0次或一次)。
[ ] :表示自定义字符分类。通常将单个字符和字符分类组合起来使用。如:[%w_]表示匹配所有以_结尾的字母或者数字。
^:表示对字符集取补集。如:[^0-7]表示匹配所有八进制以外的字符。当放在模式的开头时表示从头开始匹配。
$:模式以$结尾表示匹配到结尾。
6>.模式%bxy表示匹配x作为起始字符,y作为结束字符的子串。
7>.URL编码就是将键值对中的键和值分别进行编码。核心就是对字符串中的& = + 转换成十六进制数,将空格替换成+的过程。
8>.utf8的模式匹配通过设置utf8.charpattern来定义。
9>.模式匹配中的一些注意事项:
1>>.尽可能少的使用gsub函数,因为性能不高。
2>>.尽可能使用精确的匹配模式,这样得到的结果比较精确,否则结果在某些特殊情况下未知。
3>>.可以将可能出现歧义的内容编码成别的内容(如:十六进制值),然后再进行模式匹配来获取包含歧义内容的匹配字符串。最后将歧义字符串进行还原成源字符串形式即可。

12.日期和时间具有以下特性:
1>.日期表的格式为:{year(年),month(月),day(日),hour(时),min(分),sec(秒),wday(周几),yday(一年中的第几天),isdst(是否夏时令)}。其中UTC日期表为{year=1970, month=1, day=1, hour=0, min=0, sec=0, wday=5, yday=1, isdst=false}。
2>.os.time函数就是用指定参数(日期表,为nil时就取本机日期表。日期表中的year,month,day是必须要指定的,而hour,min,sec是可选的,且hour默认值为12,min和sec默认值为0)减去UTC日期表再减去本机时区对应的秒值,从而得到一个时间戳。
3>.os.date函数特性如下:
1>>.第二个参数为时间戳。当参数为nil时就取本机当前时间戳,也就是os.time的值。
2>>.第一个参数表示格式化字符串。当参数为nil时取%c值;当参数包含!时函数得到的日期表等于UTC日期表加上第二个时间戳参数对应的日期表,否则得到的日期表等于UTC日期表加上第二个时间戳参数对应的日期表再加上本机时区对应的秒值;当参数为*t时函数返回日期表,否则就返回参数被日期表替换后的字符串结果;
3>.os.difftime函数用来返回第一个参数对应的时间戳减去第二个参数对应的时间戳的差值。

13.位和字节具有以下特性:
1>.位运算只能针对整形数值,且是整形类型的所有位(标准整形是64位,精简整形是32位)进行操作。其中常见的位操作符如下:
&:按位与操作。
|:按位或操作。
~:按位异或或者一元取反操作。
>>:逻辑右移操作。
<<:逻辑左移操作。
2>.整形数除以2的幂得到的结果等价于整形数向右移动幂次位。如:1234 / (2^4) == 1234 >> 4。
3>.整形数向右移动n位的结果等价于整形数向左移动-n位。如:1234 >> 4 == 1234 << -4。
4>.整形数的位数小于移动位数时,结果等于0。如:1234 >> 80 == 0。
5>.64位整形数中只想操作32位整形数的话,可以对64位整形数向右移动32位来忽略高32位,从而达到操作32位整数的效果。
6>.整形数默认都是有符号整形数。如果想要得到无符号整形数的话,可以使用%u或者%x来转换成字符串的形式进行显示;如果想要以无符号整形数的方式进行比较话,可以使用math.ult函数来比较第一个参数是否小于等于第二个参数,当然也可以使用符号位的掩码0x8000000000000000来对整数的符号位进行过滤后再进行比较。
7>.有符号数和无符号数的相互转换过程如下:u表示无符号整数,f表示有符号整数。
1>>.无符号转换成有符号:f=(u+0.0)%2^64。
2>>.有符号转换成无符号:u=math.tointeger(((f+2^63)%2^64)-2^63)。
8>.string.pack函数的第一个参数是格式化字符串;第二个参数是打包数据;返回值是将打包数据按照指定格式化字符串处理后的二进制字符串。
9>.string.unpack函数的第一个参数是格式化字符串;第二个参数是二进制字符串;返回结果是将二进制字符串按照指定格式化字符串处理后的打包数据和该打包数据的后一位索引值。
10>.打包和解包二进制数据中常用的格式化字符串如下所示:
1>>.针对整数而言,小写格式为有符号整数,大写形式表示无符号整数。如:i表示有符号整数,I表示无符号整数。后面都可以指定字节数(i7表示7个字节的有符号整数,I7表示7个字节的无符号整数)。
2>>.z表示以\0结尾的字符串。
3>>.cn表示定长字符串。其中n应该等于打包字符串的长度。
4>>.sn表示指定字节的字符串。当n为nil时,也就是只有s时就表示指定size_t(一般为8个字节的无符号整形数)个字节的字符串。
5>>.f表示单精度浮点数;d表示双精度浮点数;n表示lua的浮点数。
6>>.=表示机器原生的大小端模式;>表示大端模式;<表示小端模式。
7>>.!n表示当数据比n小的话,就对齐到自身大小上;否则就对齐到n上。当n为nil时,也就是为!时,则对齐方式为机器默认的对齐方式。其中对齐大小只能是2的整数次幂,否则就会抛出异常。

14.尽可能使用table.concat函数来做字符串的拼接操作,而不要用“…”来做。理由如下:
1>>.table.concat类似于java中的StringBuilder字符串缓冲区。它会将第一个参数表示的表中的每一个字符串元素进行拼接,字符串元素之间用第二个参数表示的分割符进行分割,从而得到一个新的字符串。
2>>."…“操作符会将左右两边的字符串进行拼接,从而得到一个新的字符串。
3>>.由于字符串是不可变类型,所以每一个拼接产生的字符串都是会额外分配内存的新字符串。当参与拼接的字符串比较多时,table.concat相对于”…“而言,由于产生的临时字符串会更少,所以占用的内存会更小,拼接时移动的内存也更小,速度也更快。根据我写的测试用例来看:在执行100000次字符串拼接操作中,”…“的时间消耗是table.concat的66倍多;”…"的内存消耗是table.concat的1.7倍多。

15.lua通常适合用来作为数据文件。因为相较于其他编程语言以及xml,json,csv等,lua运行速度更快,占用内存更低。使用时需要注意一下几点:
1>.将数据文件以lua的表结构来进行表示。
2>.lua中的每一行数据之间可能同字段名存在相同的值。此时就需要将这些相同值的数据信息抽离出来作为公共表数据。然后针对公共表数据按照每一行存在相同值的数据再次进行抽离,得到进一步的公共基础表数据。然后使公共表数据中相同数据信息从公共基础表中获取,原始每一行数据中相同数据信息从公共表中获取。这样处理后,数据文件中进行了大量相同属性值得去重操作,从而降低数据文件的存储大小,同时也降低了数据文件运行时内存。
3>.将属性字段名抽离出来用索引形式表示,从而得到一个属性字段表。然后将每一行数据中用到的字段名使用索引值来表示。在取值时索引值会从属性字段表中取出属性名,进而通过属性名获取属性值。这样处理后,表的存储key从string变成int类型,从而降低表的大小,进而降低数据文件的存储大小,同时也降低了数据文件运行时内存。

16.lua在序列化数据时,通常具有以下特点:
1>.针对数值类型,布尔类型,空值类型,字符串类型,可以使用%q来对数据进行安全形式的序列化操作。其中数值类型会将整形值按照%d输出,浮点值按照%a保留精度形势输出;布尔类型输出true或者false;空值类型输出nil;字符串类型会将里面的特殊字符串以及转义字符等进行转义等处理。
2>.保存不带循环的表时只需要按照表中key-value键值对输出就行。保存带循环的表时就需要有一个保存表为key,表名为value的save表,用来对已经处理过的表直接返回表名进行输出。代码如下:

--保存不带循环的表
function Serialize(o)
	local t = type(o)
	if t == "number" or t == "string" or t == "boolean" or t == "nil" then
		io.write(string.format("%q", o))
	elseif t == "table" then
		io.write("{\n")
		for k, v in pairs(o) do
			io.write("    ", k, " = ")
			Serialize(v)
			io.write(",\n")
		end
		io.write("}\n")
	else
		error("cannot serialize a " .. t)
	end
end

-- 保存带有循环表或者共享字表的表
function BasicSerialize(o)
	return string.format("%q", o)
end

function Serialize(name, value, saved)
	saved = saved or {}
	io.write(name, " = ")
	local t = type(value)
	if t == "number" or t == "string" then
		io.write(BasicSerialize(value), "\n")
	elseif  t == "table" then
		if saved[value] then
			io.write(saved[value], "\n")	-- 使用之前的名称
		else
			saved[value] = name	-- 保存名称供后续使用
			io.write("{}\n")	-- 创建新表
			for k, v in pairs(value) do		-- 保存表字段
				k = BasicSerialize(k)
				local fname = string.format("%s[%s]", name, k)
				Serialize(fname, v, saved)
			end
		end
	else
		error("cannot serialize a " .. t)
	end
end

17.编译和异常处理时,具有以下特点:
1>.luac是独立解释器中提供预编译lua源码的命令,在lua api中也可以使用string.dump函数来对参数表示的函数进行预编译,预编译后生成的是二进制文件(也称为字节码文件)。由于lua的load,loadfile等接口中支持二进制文件和文本文件的加载方式,所以预编译生成的二进制文件可以像lua源码文件一样被lua虚拟机解释执行的。
2>.assert函数会对第一个参数进行真假判定,当为假(false或者nil)时就会断言失败并将第二个参数作为错误信息输出,没有第二个参数时就默认错误信息为"Assertion failed!";当为真时就会断言成功并将所有的参数进行返回。
3>.lua语言将所有的独立代码段当作匿名可变长参数函数的函数体。如:“a = a + 1"等价于"function(…) a = a + 1 end”。且这些独立的代码段在编译时不会进行定义,只有在运行时才进行定义。
4>.load函数从字符串或者函数中编译代码段。编译失败时返回nil和异常信息;编译成功时返回包含编译代码段的函数。load这个函数要慎用,因为它性能低,有时还会出现莫名奇妙的问题。参数定义如下:
1>>.第一个参数表示需要编译的代码段(也就是字符串或者函数)。
2>>.第二个参数表示代码段名称,在发生异常时随异常信息一起返回。
3>>.第三个参数表示代码段编译方式(b表示二进制方式,t表示文本方式,bt表示默认的二进制以及文本方式)。
4>>.第四个参数表示代码段编译环境(默认是全局环境)。
5>.loadfile函数从文件中编译代码段。编译失败时返回nil和异常信息;编译成功时返回包含编译代码段的函数。
6>.dofile函数实际上就是执行了一次loadfile函数进行编译代码段,然后调用loadfile返回的函数从而达到执行文件的目的。
7>.error函数用来抛出异常对象。其中参数一表示异常对象;参数二表示出错层级,error函数所在层级为1,调用error函数的函数层级为2,以此类推。
8>.pcall函数用来以保护形式调用参数表示的函数。当被调用函数抛出异常(自己主动调用或者系统调用error函数)时,pcall会返回false以及异常对象;相反就会返回true以及被调用函数的所有返回值。
9>.xpcall函数用来以保护形式调用参数一表示的函数。当被调用函数抛出异常(自己主动调用或者系统调用error函数)时,xpcall会返回false以及由第二个参数表示的异常处理函数返回的异常对象; 相反就会返回true以及被调用函数的所有返回值。

18.模块和包相关特点如下:
1>.lua中的搜索路径是一组模板,其中每个模板就是一个查找路径。模板之间使用;分割,并且模板内包含一个?用来被模块名所替换。常见特性如下:
1>>.package.path是lua文件的搜索路径。如果没有定义LUA_PATH_5_3或者LUA_PATH环境变量,搜索路径值就是默认路径值,否则就是环境变量中的;;替换成默认路径后获取的路径值。
2>>.package.cpath是c标准库的搜索路径。如果没有定义LUA_CPATH_5_3或者LUA_CPATH环境变量,搜索路径就是默认路径值,否则就是环境变量中的;;替换成默认路径后获取的路径值。
3>>.package.searchpath函数会从第二个参数表示的搜索路径中来查找第一个参数表示的模块名。查找成功时返回该模块名的路径,否则就返回nil和错误信息。
2>.package.searchers表中记录着全部搜索器,且每个搜索器都会返回一个加载函数。可以使用默认的4个搜索器,也可以根据自己的需求定义搜索器并添加到这个表中。其中默认4个搜索器功能分别如下:
1>>.package.searchers[1]存储的是预加载的搜索器。当搜索指定的模块名时,该搜索器会从package.preload表中查找该模块名的存储结果作为加载函数返回。
2>>.package.searchers[2]存储的是lua文件的搜索器。当搜索指定的模块名时,该搜索器会从package.path查找路径下查找该模块名的lua文件是否存在,如果存在就会以loadfile函数编译lua文件并返回包含该lua代码段的加载函数,否则返回nil。
3>>.package.searchers[3]存储的是c标准库的搜索器。当搜索指定的模块名时,该搜索器会从package.cpath查找路径下查找该模块名的c标准库是否存在,如果存在就会以package.loadlib函数编译c标准库并返回包含"luaopen_模块名"的加载函数,否则返回nil。
4>>.package.searchers[4]存储的是子模块的搜索器。当搜索指定的模块名时,该搜索器会将模块名中的.替换成/,然后从package.path查找路径下查找替换后模块名的lua文件是否存在,如果存在就会以loadfile函数编译lua文件并返回包含该lua代码段的加载函数;否则该搜索器会将模块名中的.替换成_,然后 从package.cpath查找路径下查找替换后模块名以及每个子项模块名的c标准库是否存在,如果存在就会以package.loadlib函数编译c标准库并返回包含"luaopen_替换后模块名"的加载函数,否则返回nil。
3>.require函数在加载模块名时会先从package.loaded表中查看该模块名是否已经被加载过(就是"package.loaded.模块名"不等于nil也不等于false),如果已经被加载过就直接返回结果值;否则就会遍历package.searchers表中所有的搜索器。当所有搜索器都没有获取到加载函数时,require就会抛出异常;否则就会得到一个加载函数并结束遍历搜索器。此时require会以模块名为key,加载函数的返回值(加载函数的代码段中没有返回值或者返回nil时,如果存在"package.loaded.模块名"的赋值操作的话,加载函数的返回值就等于该赋值,否则加载函数的返回值就等于true)为value形式保存在package.loaded表中并返回该value值。
4>.当要进行模块重命名时,如果是lua文件只需要将模块名修改成不一样的模块名就行;如果是c标准库文件就必须将模块名后面使用"-扩展模块名"来组合使用。

19.迭代器具有以下特性:
1>.迭代器就是由工厂函数生成的包含状态变量的闭包函数。
2>.泛型for的结构为“for varlist in explist do block end”,其中varlist表示变量列表,explist表示为表达式列表。
泛型for的执行流程如下:
1>>.对in后面的表达式列表进行求值,依次获取迭代器函数,不可变状态以及控制变量。当表达式列表中具有函数调用时,除了最后一个函数可以返回多个变量值,其他函数都是只能返回一个变量值。
2>>.将不可变状态以及控制变量传递给迭代器函数来获取返回值并赋值给变量列表。当变量列表中第一个变量(也就是控制变量)为nil时就会结束遍历,否则就会继续2>>的过程。
泛型for的执行流程转换成等价代码实现如下:

-- _f表示迭代函数,_s表示不可变状态,_var表示控制变量
do
	local _f, _s, _var = explist
	while true do
		local var_1, ... , var_n = _f(_s, _var)
		_var = _var1
		if _var == nil then break end
		block
	end
end

3>.泛型for的迭代器也叫做生成器(生成迭代的元素),而真正的迭代器是对迭代的元素进行函数调用的。两者都要经过函数调用,但是生成器模式的更加灵活(可以迭代内部指定break,return等)。

20.元表和元方法具有以下特性:
1>.lua中除了字符串默认会有一个公共的元表外,其他数据类型都没有默认的元表。并且在lua中除了表类型可以设置元表外,其他的数据类型只能通过c代码或者调试库进行设置元表。
2>.元表中设置__metatable字段就可以对元表进行保护(用户不能修改元表也不能看见元表)。其中setmetatable函数会抛出异常,getmetatable函数会返回该字段的值。
3>.元方法的查找规则:按照操作数的执行顺序依次查找操作数是否存在指定元方法的元表,要是存在就按照该元方法执行并结束查找,否则所有操作数查找完毕而抛出异常。如下所示:

local a = "test"
local b = 10
-- 此时会查找a中是否存在具有元方法__add的元表,要是存在就直接调用a中的__add元方法进行操作;否则就
-- 查找b中是否存在具有元方法__add的元表,要是存在就直接调用b中的__add元方法进行操作;否则就抛出异常
local result = a + b

4>.算术运算相关的元方法有:__add(加法),__sub(减法),__mul(乘法),__div(除法),__idiv(floor除法),__unm(负数),__mod(取模),__pow(幂运算),__band(按位与),__bor(按位或),__bxor(按位异或),__bnot(按位取反),__shl(向左移位),__shr(向右移位),__concat(连接运算符)。
5>.关系运算相关的元方法有:__eq(等于),__lt(小于),__le(小于等于)。不等于,大于,大于等于这三个关系运算符lua会自动转换成等于,小于,小于等于的等价表现形式。
6>.函数库相关的元方法有:__tostring(print函数使用,自定义输出方式),__pairs(pairs函数使用,自定义遍历方式)。
7>.表相关的元方法如下:
1>>.__index表示查找元方法。当操作表中存在指定key对应的非空value时,就会直接返回该非空value;否则就会查看操作表的元表是否含有__index值,如果没有就直接返回nil;否则就看__index值是查找表还是查找函数,如果为查找函数就会将操作表和指定key值传给该查找函数并返回最终value值;否则就会从查找表中查找指定key值对应的最终value值并返回。
2>>.rawget函数是不从元表中获取数据,而是直接从操作表中获取数据。
3>>.__newindex表示更新元方法。当对操作表进行指定key的赋值操作时,如果操作表中存在指定key对应的非空value,就会直接对操作表中指定key进行赋值更新;否则就会查看操作表的元表是否含有__newindex值,如果没有就会直接对操作表中指定key进行赋值更新;否则就看__newindex值是赋值表还是赋值函数,如果为赋值函数就会调用该函数;否则就会对赋值表进行指定key的赋值操作。
4>>.rawset函数是不从元表中更新数据,而是直接从操作表中更新数据。
5>>.在实现默认值的表时,可以使用一个排除表对象作为key来关联默认值,并在原始操作表的元表中进行返回。代码如下:

-- 排除表对象作为唯一key
local key = {}
-- 所有操作表共享的元表
local mt = {__index = function(t) return t[key] end }
-- 对表设置默认值的接口
function setDefault(t, d)
	-- 由于每个{}都是一个新的对象,所以此处所有操作表都是使用同一个{}对象来保存默认值
	t[key] = d
	-- 设置操作表的元表,该元表会对操作表中唯一key进行访问来获取默认值
	setmetatable(t, mt)
end

6>>.在实现只读的表时,可以用一个空的表作为代理表,在代理表的更新元方法(__newindex)中抛出异常即可。代码如下:

-- 排除表对象作为唯一key
local key = {}
-- 所有操作表共享的元表
local mt = {__index = function(t, k) return t[key][k] end, __newindex = function(t, k, v)  error("attempt to update a read-only table", 2)   end }
-- 对表设置只读的接口
function readOnly(t)
	local proxy = {}
	proxy[key] = t
	setmetatable(proxy, mt)
	return proxy
end

7>>.在实现跟踪表访问时,可以用一个空的表作为代理表,在代理表的更新元方法(__newindex),访问元方法(__index),遍历元方法(__pairs)以及长度元方法(__len)中访问原始操作表即可。

21.面向对象编程具有以下特性:
1>.冒号操作符在定义或者调用函数时,会增加一个额外的隐藏参数(就是对象自身,类似于c++中的this)。
2>.lua中是没用类的概念的,但是我们可以使用Self语言特性来创建一个组织实例对象行为的原型对象(类似于c++中的class)即可。代码如下:

--  原型对象表示的类名
local _M = {}
-- 实例化对象接口:调用成员变量或者函数时会先从obj中获取,obj中没有就从_M中获取
function _M:new(obj)
	obj = obj or {}
	self.__index == self
	setmetatable(obj, self)
	return obj
end

--  TODO 其他行为方法和变量定义

3>.单向继承可以将__index元方法用表来实现。通过调用2>的类来实例个对象出来作为子类。代码如下:

-- 父类_M实例化对象作为子类
local _N = _M:new()
-- 子类_N实例化对象obj
local obj = _N:new()
-- 先从obj中查找,找不到时从子类_N中查找,还找不到时就从父类_M中查找
obj:test()

4>.多重继承可以将__index元方法用函数来实现。通过调用CreateClass来得到一个多重继承的子类。代码如下:

-- 从父类列表plist中查找存在指定属性k的类,并返回该类中属性k的值
local function search(k, plist)
	for i = 1, #plist do
		local v = plist[i][k]
		if v ~= nil then
			return v
		end
	end
end

-- 创建多重继承子类。其中...用来接收多个父类
function createClass(...)
	-- 子类
	local c = {}
	-- 父类列表
	local parents = {...}
	-- 在父类列表中查找子类c中不存在的属性
	 setmetatable(c, {__index = function (t, k)
	 	-- 直接返回父类中该属性的值。每次查找时性能略低。
	 	--return search(k, parents)
	 	-- 先缓存父类中该属性的值到子类中。下次查找时性能高。但是运行时修改父类中该属性的值时不能被子类所继承。
	 	local v = search(k, parents)
	 	t[k] = v
	 	return v
	 end})
	-- 将子类作为其实例对象的元表
	c.__index = c
	--定义子类构造函数:先从子类实例对象obj中查找,找不到时就从子类c中查找,找不到时就从子类的父类列表中查找
	function c:new(obj)
		obj = obj or {}
		setmetatable(obj, c)
		return obj
	end
	-- 返回子类
	return c
end

5>.类中私有性的实现方式存在以下几种:
1>>.使用两个表,其中一个表用来保存对象所有成员,另外一个表用来保存用户可以操作的公有成员。代码如下:

function new(_var1)
	 -- 这个表用来保存对象所有成员
    local obj = {}
    -- 公有成员变量
    obj.var1 = var1
    -- 私有成员变量
    obj._var2 = 10
    -- 私有成员方法
    function obj._limit()
        return obj._var2
    end
    -- 公有方法
    function obj.printLimit()
        print(obj._limit())
    end

    -- 这个表用来保存用户可以操作的公有成员
    return {
        var1 = obj.var1,
        printLimit = obj.printLimit
    }
end

local obj = new(100)
obj.printLimit()  -- 合法调用公有成员函数
print(obj.var1)  -- 合法调用公有成员变量
obj._var2  -- 非法调用私有成员变量
obj._limit()  -- 非法调用私有成员函数

2>>.使用对偶表示。也就是将实例对象作为key,属性值作为value的形式来存储到一个局部的私有属性表中。当外面访问该私有属性表时会报错,所以只能被实例对象中的接口进行访问。代码如下:

-- 局部的私有属性表:只能被当前类_M的实例化对象中的接口访问。其中key为_M的实例化对象,value为属性值
local privateArrt = {}
-- 将局部私有属性表设置成弱引用键表,这样当键(此处为类的实例对象)不再被强引用时,
-- 对应的局部私有属性表中该键对应的条目会被回收掉,从而避免内存泄漏。
setmetatable(privateArrt , {__mode="k"})

-- 原型对象表示类
local _M = {}

-- 访问私有属性的公有接口
function _M:test()
	return privateArrt[self]
end

-- 构建实例化对象接口并将该对象存储在私有属性表中,初始值为100
function _M:new(o)
	o = o or {}
	setmetatable(o, self)
	self.__index = self
	privateArrt [o] = 100
	return o
end

local obj = _M:new()
print(obj:test())  -- 合法访问公有接口来间接访问私有属性值

22.环境具有以下特性:
1>.使用local关键字定义的变量叫做局部变量,存储在局部表中。
2>.不使用local关键字定义的变量叫做自由名称(“全局变量”)。由于全局变量会扰乱代码,使代码变得复杂,所以lua5.3中就不支持全局变量。但是由于全局变量可以很好的表达全局的概念,所以lua中就使用一个局部环境表_ENV来模拟实现全局变量。
3>._G表示全局环境表。_G会把自己存储在自己的_G属性上,也就是_G._G=_G。
4>._ENV表示局部环境表。具有以下特性:
1>>.编译器会在每个代码段开头定义"local _ENV=_G"。当然用户也可以自定义_ENV(如:local _ENV=“你的值”)或者修改_ENV(如:_ENV=“你的值”)。
2>>.自由名称(“全局变量”)的赋值和取值操作都是在_ENV上面进行的。
3>>.每个_ENV只在各自的代码段生命周期内有效。如果自由名称(“全局变量”)存在多个有效的_ENV时,就会以最近的一个_ENV为准。
5>.动态设置全局环境表中的变量时,可以分两部分实现:第一部分用来定义除了最后一个变量外的变量;第二部分用来对最后一个变量进行赋值。代码如下:

function setfiled(var, val)
    local t = _G    -- 从全局环境表开始
    -- 获取变量名var中所有的变量和点
    for w, d in string.gmatch(var, "([%a_][%w_]*)(%.?)") do
        if d == "." then
            -- 不是最后一个变量就创建表
            t[w] = t[w] or {}
            t = t[w]
        else
            -- 是最后一个变量就赋值
            t[w] = val
        end
    end
end

6>.动态获取全局环境表中的变量时,可以从全局环境表中依次获取变量名的值,直到返回最后一个变量名的值。代码如下:

function getfiled(var)
    local v = _G    -- 从全局环境表开始
    -- 获取变量名中所有的变量
    for w in string.gmatch(var, "([%a_][%w_]*)") do
        -- 从表中依次获取变量的值
        v = v[w]
    end

    -- 返回最后一个变量的值
    return v
end

7>.由于lua中的自由名称(“全局变量”)不需要声明就可以使用,这样容易造成乱用以及覆盖等问题。为了解决这个问题,可以在_G的元表中对__index接口做是否有声明过的判定,没有就抛出异常,否则就直接获取_G表中的值返回;对_G的元表中__newindex接口做是否声明过的判定,有声明过就直接修改_G表中该变量的值,否则就看是否在非lua普通函数模块,是的话就直接修改_G表中该变量的值,否则就抛出异常。具体代码可以参考lua发布的strict.lua
8>.模块在声明时,为了避免自由名称(“全局变量”)污染全局环境表,解决方案如下:
1>>.使用"_ENV=模块名"方式来将模块的所有自由名称(“全局变量”)定义在这个局部环境表_ENV中,此时定义和使用模块的成员时也不必要再加上模块前缀了。
2>>.使用"_ENV=nil"方式来将模块的所有自由名称(“全局变量”)定义时抛出异常。
9>.在load和loadfile中会把_ENV作为上值参数,为了设置这个上值参数,通常有以下方法:
1>>.直接设置load和loadfile的_ENV参数值。
2>>.通过debug.setupvalue来设置load和loadfile返回的加载函数中指定1索引处的_ENV值。
3>>.通过在load和loadfile加载的代码段前面加上"_ENV=…"来将参数动态设置成_ENV的值。

23.垃圾收集具有以下特性:
1>.lua采取自动垃圾回收机制将确认为垃圾的对象(表,闭包等)进行回收。所以只提供了创建对象的接口,但没有提供删除对象的接口。当一个对象的所有强引用都置为nil时,垃圾收集器就会回收这个对象。
2>.弱引用表指的是元表中__mode属性值为k(弱引用键表)或者v(弱引用值表)或者kv(弱引用键值对表)的表。具有以下特性:
1>>.number,string,boolean等类型是强类型;表和闭包等类型是弱类型。
2>>.弱引用表中只有指定__mode值的部位并且该部位存放的对象是弱引用类型时,该对象才具有弱引用的特性。
3>>.垃圾收集器会将只有弱引用的对象进行回收。并且该对象在弱引用表中的整个条目也将被回收。
4>>.将记忆函数关联的缓存表设置成弱引用表时,不仅可以提高参数的查找速度,还可以将只有弱引用的对象对应的条目从缓存表中回收掉,避免内存泄漏。
5>>.将对偶表示的对象属性表设置成弱引用表时,不仅可以保证属性的私有性,还可以将只有弱引用的对象对应的条目从对象属性表中回收掉,避免内存泄漏。
6>>.瞬表指的是一个具有弱引用键和对该键具有强引用值的表。瞬表的弱引用键的生命周期跟该键的强引用值没关系,还是在弱表外没有强引用时垃圾收集器就会回收该弱引用键对应的条目。
3>.析构器指的是当对象调用setmetatable函数设置元表时,如果元表中含有一个非nil值的__gc属性,那么lua虚拟机就会将该对象标记为可析构对象,此时的__gc属性值就是析构器。具有以下特性:
1>>.析构器可以是任何非nil值,一般为参数是析构对象的函数。
2>>.当析构器为函数时,由于参数是析构对象,此时这个要被垃圾收集器回收的对象就又变成临时复苏对象;另一方面在析构器内部实现中也有可能将析构对象存放在全局表中,此时析构对象又变成永久复苏对象。
3>>.当同一垃圾回收期间,析构器的调用顺序是标记为可析构对象顺序的逆序。
4>>.析构对象的回收需要两次垃圾收集器的调用。第一次是将析构对象标记为已析构并放入到被析构队列中;第二次调用是从被析构队列中将已析构对象进行回收。
4>.垃圾收集器的执行过程如下:
1>>.使用lua5.2中的紧急垃圾收集机制。也就是当内存分配不足时就进行一次垃圾收集回收操作。
2>>.使用lua5.1中的增量垃圾收集机制。也就是在进行垃圾收集收回操作时不需要停止主线程的操作,可以与解释器交替执行。
3>>.使用lua5.0中的标记-清除垃圾收集机制。也就是经过标记->清理->清除->析构流程。其中每个阶段如
1>>>.标记阶段指的是将对象根节点标记为活跃。然后遍历根节点对象集合,将每个对象按照是否可达(也就是有强引用)来设置成是否活跃。
2>>>.清理阶段指的是将根节点对象集合中的不活跃且可析构状态的对象变成活跃且已析构状态对象并放入到被析构列表中。然后将弱引用表中指定弱引用部位没有强引用的对象所对应的条目进行移除。
3>>>.将根节点对象集合中不活跃的对象进行回收,活跃的对象就会清除标记以供下一轮的垃圾收集处理。
4>>>.将被析构列表中已析构对象调用析构器进行处理,然后将对象进行回收。
5>.可以使用collectgarbage函数来控制垃圾收集器。一般使用默认的设置便可,但是有些特殊情况需要手动进行设置。常见的设置如下:
1>>.stop表示停止垃圾收集器。
2>>.restart表示重启垃圾收集器。
3>>.collect表示执行一次完整的垃圾收集,回收所有的不可达(强引用为0)对象。
4>>.count表示当前的已占用内存数。
5>>.step表示垃圾收集器收集指定参数对应大小的内存。
6>>.setpause表示垃圾收集器在一次收集完成后等待指定参数对应的时间再开始新的一次收集。
7>>.setstepmul表示每分配1kb内存,垃圾收集器要回收指定参数大小内存。

24.协程具有以下特性:
1>.协程可以实现类似多线程的功能。两者区别为:
1>>.多线程在同一时刻可以并发执行多个线程,而协程只能有一个执行。
2>>.多线程可以被cpu调度来挂起或者执行,而协程必须自己主动挂起。
2>.协程的状态有四种,分别为:normal(正常),running(运行),suspended(挂起),dead(死亡)。
3>.常见的api如下:
1>>.create函数用来创建一个协程。其中参数为执行函数,返回值为挂起状态的thread类型对象(也就是协程对象)。
2>>.status函数用来查看协程状态。其中参数为thread类型对象,返回类型为协程状态字符串。
3>>.yield函数用来将一个运行状态的协程进入到挂起状态。当使用resume函数唤醒协程时就会返回resume函数传递的参数列表。
4>>.resume函数用来将一个挂起状态的协程进入到运行状态。当调用失败时就返回false+异常信息;否则如果被yiled函数挂起时就返回true+yield函数参数列表值;否则就返回true+执行函数的返回列表值。
5>>.warp函数用来创建一个协程。其中参数为执行函数,返回值为一个包含resume协程的函数。当调用该返回值时会resume协程进而调用执行函数。
5>>.running函数用来返回正在运行状态的协程。

25.反射具有以下特性:
1>.反射指的是可以对程序进行检查和修改的能力。全局相关的检查和修改可以通过环境表进行操作,局部以及堆栈调用等相关的检查和修改可以使用调试库debug进行操作。
2>.自省函数debug.getinfo的第一个参数为指定函数或者指定栈层次(getinfo函数栈层次为0,调用getinfo函数的栈层次为1,往上的函数,栈层次依次+1)关联的函数;第二个参数为指定的调试类型,默认为全部调试类型;当不存在第一个参数关联函数时返回nil,否则就返回调试数据表。
调试数据表中的元素如下所示:
1>>.source表示函数所在的文件或者代码段。
2>>.short_src表示source的精简版(最多60个字符),常用于错误信息。
3>>.linedefined表示函数定义在源代码中第一行的行号。
4>>.lastlinedefined表示函数定义在源代码中最后一行的行号。
5>>.what表示函数的类型。lua中函数的话就是"Lua",c中函数的话就是"C",lua中的代码段的话就是"main"。
6>>.name表示函数的名称。
7>>.namewhat表示函数的类型。如果没有找到就是空串,否则就是global,local,method,field等值。
8>>.nups表示函数中上值的个数。
9>>.nparams表示函数中参数的个数。
10>>.isvararg表示函数是否为可变成参数的函数。
11>>.activelines表示函数中除了空行和注释行外所有的活跃行集合。常用于调试器中,因为空行和注释行不能被断点调试。
12>>.func表示函数自身。
13>>.currentline表示函数正在执行的代码所在的行。
14>>.istailcall表示函数是否为尾调用所调起。
调试类型如下所示:
1>>.n对应的是name和namewhat。
2>>.f对应的是func。
3>>.S对应的是source,short_src,what,linedefined和lastlinedefined。
4>>.l对应的是currentline。
5>>.L对应的是activelines。
6>>.u对应的是nup,nparams和isvararg。
3>.debug.traceback函数用来返回堆栈调用信息字符串。
4>.debug.getlocal函数可以用来获取指定栈层次关联的函数中指定索引位置处的局部变量数据。具有以下特性:
1>>.第一个参数为指定栈层次(getlocal函数栈层次为0,调用getlocal函数的栈层次为1,往上的函数,栈层次依次+1)关联的函数;
2>>.第二个参数为局部变量索引值。此时有效局部变量指的是第一个参数指定的关联函数所在的作用域中可以操作的局部变量。索引值为负数(-1开始)时就会从可变长参数局部变量列表中获取该索引处的变量名"(*vararg)"和变量值;当索引值为正数(1开始)时就会获取该索引处的普通局部变量名和变量值。
3>>.如果第一个参数关联的函数不存在就抛出异常;否则如果局部变量的索引超过了getlocal函数当前可以操作的局部变量索引时就会返回nil,否则就会返回变量名+变量值。
4>>.本例子介绍了getlocal函数中可以操作的局部变量索引和可变成参数变量的操作。代码如下:

-- 访问局部变量
function foo(a, b, ...)
    local idx = 1
    while true do
        -- 栈层次为1就是foo函数,该函数中可以操作的局部变量是从foo定义开始到getlocal函数调用为止。由于varName,varValue
        -- 在调用getlocal函数时并没有声明,所以不能操作该局部变量
        local varName, varValue = debug.getlocal(1, idx)
        if varName == nil then
            if idx > 0 then
                -- 正数对应的普通局部变量操作完后就开始操作可变成参数局部变量
                idx = 0
            else
                -- 没有当前索引对应可变成参数局部变量,也没用普通局部变量时就结束查找
                break
            end
        else
            print(varName, varValue)
        end

        if idx <= 0 then
            -- 获取可变成参数局部变量中下一个变量值
            idx = idx - 1
        else
            -- 获取下一个普通局部变量索引
            idx = idx + 1
        end
    end
end

foo(100, 200, "zjz", true, {}, function () end, coroutine.create(function () end))

运行结果为:

a	100
b	200
idx	3
(*vararg)	zjz
(*vararg)	true
(*vararg)	table: 000000000072a100
(*vararg)	function: 000000000072ce40
(*vararg)	thread: 0000000000725e08

5>.debug.setlocal函数可以用来设置指定栈层次关联的函数中指定索引位置处局部变量的值。具有以下特性:
1>>.第一个参数为指定栈层次(setlocal函数栈层次为0,调用setlocal函数的栈层次为1,往上的函数,栈层次依次+1)关联的函数;
2>>.第二个参数为局部变量索引值。此时有效局部变量指的是第一个参数指定的关联函数所在的作用域中可以操作的局部变量。索引值为负数(-1开始)时对应的是可变成参数局部变量;否则索引值为正数(1开始)时对应的是普通局部变量。
3>>.如果第一个参数关联的函数不存在就抛出异常;否则如果局部变量的索引超过了setlocal函数当前可以操作的局部变量索引时就会返回nil,否则就会返回变量名。
6>.debug.getupvalue函数可以用来获取指定闭包函数中指定索引位置处的上值变量的数据。具有以下特性:
1>>.第一个参数为指定闭包函数;
2>>.第二个参数为上值变量索引值。
3>>.如果上值变量的索引超过了闭包函数当前可以操作的上值变量索引时就会返回nil,否则就会返回变量名+变量值。
4>>.本例子介绍了getupvalue函数中可以操作的上值变量索引以及修改对应索引位置处的上值变量值。代码如下:

local func = function (a, b, c)
    local e = 20
    f = 30
    return function (d)
        d = 10
        return a + b + c
    end
end

-- 获取闭包函数
local func2 = func(1, 2, 3)

local i = 1
while true do
    -- 参数为闭包函数,索引为上值索引。当索引不存在时返回nil
    local n, v = debug.getupvalue(func2, i)
    if n == nil then
        break
    end
    
	debug.setupvalue(func2, i, v + 10)
    print(n, v)
    i = i + 1
end

运行结果如下:

a	1
b	2
c	3
36

7>.debug.setupvalue函数可以用来设置指定闭包函数中指定索引位置处上值变量的值。具有以下特性:
1>>.第一个参数为闭包函数。
2>>.第二个参数为上值变量索引值。
3>>.如果上值变量的索引超过了闭包函数当前可以操作的上值变量索引时就会返回nil,否则就会返回变量名。
8>.debug.sethook函数用来设置指定的事件触发时调用指定的钩子函数。参数如下:
1>>.第一个参数为钩子函数;
2>>.第二个参数为事件掩码字符串。其中"c"代表函数调用事件call或者tail-call;"r"代表函数返回事件return;“l"代表执行新行代码事件line;”"代表执行指令(lua中的opcode)事件count。
3>>.第三个参数为额外扩展参数。当事件为count时,该参数表示触发count事件所需要的指令数。
4>>.当所有参数都为nil时就会关闭钩子。
9>.debug.gethook函数用来获取钩子信息。当不存在一个非nil的钩子函数时返回nil和0,否则就会返回钩子函数,事件掩码字符串和额外的扩展参数数值。
10>.debug.debug函数用来提供一个可以执行任意lua语言命令的提示符。
11>.调试库中的一些函数的第一个参数可以接收一个协程对象。因此我们可以在外部对这个协程对象进行相关的调试操作。此时调试函数中的栈层次顺序就是协程对象的执行函数从内部到外部,从下到上的顺序,且层次索引从0开始。
演示代码如下所示:

-- 执行函数在协程co中的栈层次索引为1
co = coroutine.create(function ()
    -- 此处的局部变量索引为1
    local x = 10
    -- 挂起函数在协程co中的栈层次索引为0
    coroutine.yield()
    -- 唤醒挂起的co后,上面的yield执行结束并从栈层次中移除,此时的error函数在协程co中的栈层次为0
    error("some error")
end)

-- 第一次唤醒创建时挂起的协程co并调用执行函数并在该函数内部调用yield进行阻塞
print(coroutine.resume(co))
-- 获取协程co中的堆栈信息,由于协程和主程序不再同一个栈,所以不能获取主程序的堆栈信息
print(debug.traceback(co))
-- 再次调用唤醒yield挂起的协程,并调用error函数挂起协程
print(coroutine.resume(co))
-- 此时的error函数在协程co上的栈层次为0,而执行函数在协程co上的栈层次为1
print(debug.getlocal(co, 1, 1))

运行结果如下所示:

true
stack traceback:
	[C]: in function 'yield'
	Test4.lua:6: in function <Test4.lua:2>
false	Test4.lua:8: some error
x	10

12>.在沙盒环境下执行文件可以让程序更加健壮,不容易被Dos攻击。一个好的沙盒环境具有以下特性:
1>>.沙盒环境表必须是内部独立的表,这样外界就不能对lua中的环境表(如_G表)进行恶意操作。
2>>.为了避免执行文件中包含大量的指令调用(就是lua调用c函数形成一个opcode指令)而进行CPU消耗攻击,可以设置执行指令事件的钩子函数,当在钩子函数中超过指令步长上限时就禁止操作。
3>>.为了避免执行文件内存过大或者使用少量的指令创造出大量内存等手段进行内存消耗攻击,可以在钩子函数中检测内存消耗,当超过内存上限时就禁止操作。
4>>.虽然沙盒环境表是独立的表,但是像string这种库却可以在沙盒环境中运行。为了避免恶意使用string等库进行内存消耗攻击,可以在沙盒环境中增加一个合法函数表,当在钩子函数中检测到不合法的函数调用时就禁止操作。
5>>.一个包含上述特性的展示代码如下所示:
沙盒环境文件sandbox.lua的代码如下所示:

local debug = require "debug"

-- 最大能够使用的内存(单位KB)
local memlimit = 1000
-- 检查内存是否够用
local function checkmem()
	if collectgarbage("count")  > memlimit then
		error("scripts uses too much memory!")
	end
end

-- 设置授权的函数
local validfunc = {
	[string.upper] = true,
	[string.lower] = true,
	-- 其他授权函数
}

-- 最大能够执行的指令步长
local steplimit = 1000
-- 当前使用的指令步长
local step = 0

-- 执行指令事件对应的钩子函数
local function hook(event)
	-- 检查非法函数,避免内存消耗攻击
	if event == "call" then
		-- getinfo的栈层次为0,hook函数栈层次为1,调用hook函数的地方栈层次为2。
		local info = debug.getinfo(2, "fn")
		-- 使用非法函数
		if not validfunc[info.func] then
			error("calling bad function:" .. (info.name or "?"))
		end
	end

	-- 检查内存消耗,避免内存消耗攻击
	checkmem()
	
	-- 检查指令步长,避免CPU消耗攻击
	step = step + 1
	if step > steplimit  then
		error("scripts uses too much cpu!")
	end
end

-- 加载代码段:第一个参数arg[1]为执行文件;第三个参数{}为内部环境表,从而对执行文件的所有操作
-- 都是在这个内部环境表中进行,避免对环境表(_G等)进行非法操作。
local f = assert(loadfile(arg[1], "t", {}))
-- 设置钩子函数,每当执行了100个指令(lua调用c#形成的opcode指令)时就会调用该钩子函数。
debug.sethook(hook, "", 100)
-- 运行代码段
f()

– 使用沙盒环境文件sandbox.lua调试执行文件main-prog的代码如下所示:

--  使用lua命令执行
lua sandbox main-prog
发布了81 篇原创文章 · 获赞 39 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/zjz520yy/article/details/96504913