最近使用Lua过程中的一些笔记记录,零零散散的,主要有以下这么些内容:
版本兼容性处理
常接触到的Lua版本有5.1、5.2和5.3,在Lua中可以通过_VERSION
获取版本:
1 | if _VERSION == "Lua 5.3" then |
Lua5.1、5.2和5.3的一些常会遇到的API差异:
5.1->5.2
loadstring
改为load
setfenv/getfenv
改为_ENV
5.2->5.3
unpack
改为table.unpack
一种较为简单的兼容API差异的方法是增加类似这样的代码:
1 | load = load or loadstring |
pack和unpack
在Lua中,table和逗号分隔的多个值的互相转化,即将多个值打包成table或者是把table解包成多个值。以下是几个具体的使用场景:
变长参数的函数
将多个值转为table,使用{...}
:
1 | local function func(...) |
将会输出:
1 | got 5 arguments |
另一种取值(遍历)的方法是使用select
,当select
的参数为"#"
时,返回这一组参数的长度,当参数为整数时,表示截取此整数位置及其后的所有元素:
1 | local function func(...) |
将会输出:
1 | got 6 arguments |
将table值赋给多个变量
1 | local unpack = unpack or table.unpack |
将会输出:
1 | a = 3 |
处理有多个返回值的函数
只有放在最后的时候才能接收到所有的返回值
1 | local function multiRet() |
将会输出:
1 | a = ret1 b = ret2 c = nil |
迭代器
在Lua中,习惯了用ipairs
和pairs
来做遍历,其实它们也只是比较特殊的函数而已:
1 | function ipairs(t) end |
传入参数是一个表,该函数返回三个值,依次是一个迭代函数f
、表t
和0
,因此可以使用:
1 | for i,v in ipairs(t) do |
这种格式来迭代(1,t[1]), (2,t[2]) ...
这样成对的值,直到没有更多的整数索引。
1 | function pairs(t) end |
传入参数是一个表,该函数返回三个值,依次是函数next
、表t
和nil
,因此可以使用:
1 | for k,v in pairs(t) do |
这种格式来迭代表中所有的键值对。
函数next
:
1 | function next(table, index) end |
可用next
遍历整个表,第一个参数是表,第二个参数是表内的一个索引。next
会返回表的下一个索引和关联的值。当第二个参数是nil时,next
会返回一个初始索引和其关联的值。当第二个参数是表的最后一个索引时,或者当表是空表而第二个参数是nil时,next
会返回nil。如果第二个参数空缺,则认为是nil,此时你可以用next(t)
来判断一个表是否为空表。表内索引值的迭代顺序是不确定的,即便是数字型索引。如果在使用next
遍历过程中给遍历中的表增加新的值,将会出现为定义行为。但是你可以修改甚至清除当前已有的值。
使用next
方法,则pairs
函数可理解为:
1 | function pair(t) |
raw方法
使用rawget
和 rawset
避免触发__index
和__newindex
方法:
1 | local t = {a = "a"; b = "b"; c = "c"} |
将会输出:
1 | c |
除此之外还有rawequal
,避免触发__eq
方法:
1 | local eq = function(t1,t2) |
将会输出:
1 | t1 == t2 -> true |
package相关
查找路径
当require
某个脚本被告知module not found
或者不确定脚本路径从哪一级写起时,可以使用以下方法显示当前的查找路径。
1 | print(package.path) |
package.path
中记录了所有的包含路径。如果要增加某个路径,只需像拼接字符串那样接在后边即可:
1 | package.path = package.path .. ";/SomeFolder/?.lua" |
模块重新加载
对于已经加载的Lua模块,如果再次require
会直接得到第一次加载时的内容。加载过的模块都可以在package.loaded
和package.preloaded
中找到。可以手动将其置为nil,然后再重新require
。使用这种方法,可以在运行时不重启程序,修改Lua脚本并应用修改内容,用来调试Lua代码非常方便。
文件夹m下有文件mod1.lua 内容如下:
1 | func = function() |
测试代码,可以在命令行测试:
1 | require ("m.mod1") |
这种操作只会影响到下次require
时执行的动作,但不会影响到已经加载到内存中的内容。
环境
默认情况下,全局变量都保存在一个名为_G
的表里,也可以通过遍历_G
获取全部的全局变量/函数,如:
1 | t = {"a","b","c"} |
setfenv和getfenv
这个_G
就是一个默认的函数环境,有时候我们可能会有一些需求要使用其它的环境而非默认的_G
,在5.1版本的lua中我们可以使用setfenv
和getfenv
来设置和获取环境。
setfenv
接收两个参数,第一个参数为一个函数或者一个数字,第二个参数为设置的目标环境表。当第一个参数为函数时,表示设置该函数的环境,若第一个参数为数字(1、2、3…),1表示当前函数,2表示更外一层即调用当前函数的函数,以此类推。
1 | local newEnv = {} |
_ENV
5.2及更高版本的lua废弃了setfenv
和getfenv
,取而代之使用_ENV
来设置环境,如:
1 | local newEnv = {} |
print
是新的环境中的函数。
沙盒环境
制作沙盒环境,只能访问到希望访问到的函数,并且对全局变量的修改也都是在临时的新环境中进行,上边的例子就是一种应用,将希望在新环境中使用的函数(全局的print
),使用upvalue
的形式(prt
)引用到新的环境中newEnv.print
。
如果在新环境中使用_G
的函数,另一种方法是:
1 | local newEnv = {_G = _G} |
或者直接将_G
放到元表中,如果当前环境中未定义某个方法,则到_G
中去找:
1 | local newEnv = setmetatable({},{__index = _G}) |
print
是新的环境中的函数,而tostring
是原来的函数。
封装模块
当编写一些模块时,模块内的一些方法或者成员我们希望它们是私有的,即不被外部访问到,通常在模块内可以将他们声明为local
,但是这么做需要严格控制其定义的顺序。另一种方法是在模块内部使用一个新的环境:
1 | local mod = {} |
加载该模块后,可以调用模块方法及全局的方法,但是不能调用到私有的方法:
1 | local m = require "m" |
string模块
Lua中最常用的模块之一,用于字符串相关的操作,这里记录其中几个函数的使用:
string.dump
可以将一个函数序列化成字符串,然后在需要的时候将其加载:
1 | local GetSum = function(...) |
string.gsub
即string.gsub(s, pattern, repl [,m])
。简单来说,就是字符串匹配替换,将字符串内的某些内容替换成希望的内容,如:
1 | local str = "hello world hello game hello" |
但是gsub
的功力远不限于此,首先第二个参数pattern
可以是正则表达式,正则表达式中用%
来转义,并且以%n
的形式在repl
中取到捕获组:
1 | local str = "hello world hello game hello" |
第三个参数更加灵活,可以是字符串、表或者函数:
如果是字符串,则直接替换,并且是以
%n
这样的形式来获取匹配捕获的内容。如果是表,则以匹配到的内容为key,到表里去取值并返回取到的值:
1
2
3
4
5local str = "hello world hello game hello"
print(str)
local t = { hello = "你好", world = "世界", game = "游戏"}
local newStr = string.gsub(str,"%w+",t)
print(newStr) -- 你好 世界 你好 游戏 你好如果是函数,则以匹配到的内容为参数,调用函数并使用函数的返回值:
1
2
3
4
5
6
7local str = "hello world hello game hello"
print(str)
local f = function(arg)
return string.upper(arg)
end
local newStr = string.gsub(str,"%w+",f)
print(newStr) -- HELLO WORLD HELLO GAME HELLO
在使用字符串的过程中,常常会用到分割字符串的功能,也可以通过gsub
来实现:
1 | function SplitString(str, sep) |
string.gmatch
即string.gmatch(s, pattern)
,此函数返回的是一个迭代器函数,每次调用时即会返回在s
中用pattern
匹配到的下一个字符串,直到再没有更多的匹配时返回nil
。
这个gmatch
返回的是一个迭代器函数,有点像前边说到的ipairs
和pairs
此类。以下是一个示例:
1 | local str = "hello world hello game hello" |
时间相关计算
os.time
可以获取一个时间戳,参数为空时返回当前时间戳,参数为一个表时,根据表内指定的时间返回对应时间戳:
1 | print(os.time()) |
表内year
、month
、day
为必填字段,hour
、min
、sec
为选填字段,缺省为12:00:00
,isdst
表示是否夏时令也是选填字段,缺省为false。
os.date
即os.date ([format [, time]])
,按照指定的格式和时间,获取格式化的时间。第二个参数为时间戳,如果是缺省则表示当前时间。第一个参数是一个字符串表示的格式,其中包括的一些常用的标签如下:
标签 | 描述 |
---|---|
%a |
星期的缩写,如 “Wed” |
%A |
完整的星期,如“Wednesday” |
%b |
月份的缩写,如“Sep” |
%B |
完整的月份,如“September” |
%c |
时间和日期,如“09/16/98 23:48:10” |
%d |
月中的第几天[01-31] |
%H |
24小时制的小时数[00-23] |
%I |
12小时制的小时数[01-12] |
%M |
分钟[00-59] |
%m |
月份[01-12] |
%p |
“am”或“pm” |
%S |
秒[00-61] |
%w |
周内的第几天[0-6 = Sunday-Saturday] |
%x |
日期,如“09/16/98” |
%X |
时间,如“23:48:10” |
%Y |
年份,如“1998” |
%y |
二位的年份,如“98” [00-99] |
%% |
字符“%” |
一个简单的示例:
1 | print(os.date("%Y-%m-%d %H:%M:%S")) -- 2018-06-23 13:22:55 |
除了上边表里的标签外,还有两个特殊的标签,一个是!
,另一个是*t
:
如果使用了!
,则会按照格林尼治时间来输出日期格式(无论当前系统在哪个时区,输出的内容会是相同的):
1 | print(os.date("%Y-%m-%d %H:%M:%S")) -- 2018-06-23 13:25:54 |
如果使用了*t
,那么输出的不再是一个字符串,而是一个表:
1 | local t = os.date("*t") |
将会输出
1 | yday = 174 |
可以借助difftime来获取本地的时区
1 | local now = os.time() |
log输出调试信息
输出调用栈
最简单实用的,输出当前调用栈:
1 | print(debug.traceback()) |
后边再给一个例子。
debug.getinfo
获取更多的调试信息,getInfo
需要传入一个整数表示函数调用级别,0
表示自身(getinfo
),1
表示直接调用者,2
表示更上一层的调用函数,以此类推,以下是一个示例,func5
调用了func4
,数字递减直到func2
调用了func1
,在func1
的内部输出调试信息:
1 | local printDebugInfo = function(level) |
输出内容为:
1 | stack traceback: |
错误处理
error和assert
编写一些底层函数或者工具方法时,需要增加一些错误提示或者断言,以使调用者在错误地使用这些方法后可以很快知道出了什么问题,可以使用error
来实现此功能:
1 | local funcAdd = function(a,b) |
将会输出:
1 | lua: test.lua:3: b is not a number |
assert
其实可以更简化error
。其语法其它编程语言中的断言很相似,刚才的funcAdd
可以简化为:
1 | local funcAdd = function(a,b) |
输出的内容是相同的。
pcall
Lua中的异常处理机制,类似于其它语言中的try...catch
,pcall
以保护模式调用函数,并返回函数是否成功的调用,以下是一个示例:
1 | local funcAdd = function(a,b) |
state
用来记录函数调用是否成功,ret
用来记录函数的返回值。将会输出
1 | Calling funcAdd(3,6) ... |
xpcall
xpcall
与pcall
类似,比pcall
多传入一个错误处理函数,通常可以用来打印出调用栈:
1 | local funcAdd = function(a,b) |
输出的内容如下:
1 | Calling funcAdd(8,nil) ... |
弱引用表
Lua的GC机制,当变量有任何引用时,就不会被清理掉。但是在很多情况下,一些变量我们不再使用了,但是因为有引用,Lua的GC不会将其识别为可清理的垃圾。使用弱引用表可以解决此问题,弱引用表可以设置键或者值对变量弱引用,通过其元表的__mode
字段来设置,__mode
的值是一个字符串,若该字符串包含k
,则该表的键为弱引用,若该字符串包含v
,则该表的值为弱引用,若同时包含kv
,则键和值都是弱引用,可以看一下的一组示例:
1 | local t1 = {} |
将会输出:
1 | t1[key] = v1 |
上边的四个表,t1
是普通的表,t2
、t3
、t4
分别是键弱引用、值弱引用、键和值都弱引用的表。当执行了GC之后,kt
和vt
会被保留,而其余的{"v1"}
到{"k4"}
这些表如果没有被引用就都会被清理掉(普通表会引用它们,到是对应弱引用的表不会)。
t1
是普通的表,没有任何变化,两对键值都还在;t2
的键是弱引用,因此t2[{"k2"}]=vt
这一对键值,{"k2"}
会被GC清理掉,这一对键值会被清理掉;t3
的值是弱引用,因此t3[kt]={"v3"}
这一对键值,{"v3"}
会被GC清理掉,这一对键值会被清理掉;t4
的键和值都是弱引用,所以当{"k4"}
、{"v4"}
会被GC清理掉,因此最后t4
变成了一个空表;
tableext
在最近使用Lua过程中,整理了一些操作table的工具方法,作为table的扩展,放在一个名为tableext(table extension)的模块中,其中包含了一些table打印输出、序列化、深拷贝、合并或分割的操作,以及filter、map、reduce等方法,详见github:https://github.com/aillieo/tableext。
REFERENCE
http://www.lua.org/manual/5.3/
http://www.lua.org/manual/5.2/
http://www.lua.org/manual/5.1/
http://lua-users.org/wiki/EnvironmentsTutorial
http://lua-users.org/wiki/StringLibraryTutorial
http://www.lua.org/pil/22.1.html