Lua笔记

最近使用Lua过程中的一些笔记记录,零零散散的,主要有以下这么些内容:

版本兼容性处理

常接触到的Lua版本有5.1、5.2和5.3,在Lua中可以通过_VERSION获取版本:

1
2
3
4
5
6
7
if _VERSION == "Lua 5.3" then
print("5.3")
elseif _VERSION == "Lua 5.2" then
print("5.2")
elseif _VERSION == "Lua 5.1" then
print("5.1")
end

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
2
load = load or loadstring
unpack = unpack or table.unpack

pack和unpack

在Lua中,table和逗号分隔的多个值的互相转化,即将多个值打包成table或者是把table解包成多个值。以下是几个具体的使用场景:

变长参数的函数

将多个值转为table,使用{...}

1
2
3
4
5
6
7
8
9
local function func(...)
local args = {...}
print("got " .. #args .. " arguments")
for i,v in ipairs(args) do
print("arg" .. i .. " = " .. tostring(v))
end
end

func(1,2,3,{},"apple")

将会输出:

1
2
3
4
5
6
got 5 arguments
arg1 = 1
arg2 = 2
arg3 = 3
arg4 = table: 0x7f99e94044c0
arg5 = apple

另一种取值(遍历)的方法是使用select,当select的参数为"#"时,返回这一组参数的长度,当参数为整数时,表示截取此整数位置及其后的所有元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local function func(...)
local total = select("#",...)
print("got " .. total .. " arguments")
for i = 1, total do
print("arg" .. i .. " = " .. tostring(select(i,...)))
end

print("last arg " .. " = " .. tostring(select(-1,...)))

local argsFrom2 = {select(2,...)}
print(" --- \nargs form the 2nd: ")
for i,v in ipairs(argsFrom2) do
print("arg" .. i .. " = " .. tostring(v))
end

end

func("heheda",1,2,3,{},"apple")

将会输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
got 6 arguments
arg1 = heheda
arg2 = 1
arg3 = 2
arg4 = 3
arg5 = table: 0x7f9483d04130
arg6 = apple
last arg = apple
---
args form the 2nd:
arg1 = 1
arg2 = 2
arg3 = 3
arg4 = table: 0x7f9483d04130
arg5 = apple

将table值赋给多个变量

1
2
3
4
5
6
local unpack = unpack or table.unpack
local t = {3,100,"good"}
local a, b, c = unpack(t)
print("a = ".. a)
print("b = ".. b)
print("c = ".. c)

将会输出:

1
2
3
a = 3
b = 100
c = good

处理有多个返回值的函数

只有放在最后的时候才能接收到所有的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local function multiRet()
return "ret1","ret2"
end

local a,b,c
local prt = function()
print("a = " .. tostring(a) ..
" b = " .. tostring(b) ..
" c = " .. tostring(c))
end

a,b,c = multiRet()
prt()
a,b,c = multiRet(), "newValue"
prt()
a,b,c = "newValue", multiRet()
prt()

将会输出:

1
2
3
a = ret1   b = ret2   c = nil
a = ret1 b = newValue c = nil
a = newValue b = ret1 c = ret2

迭代器

在Lua中,习惯了用ipairspairs来做遍历,其实它们也只是比较特殊的函数而已:

1
function ipairs(t) end

传入参数是一个表,该函数返回三个值,依次是一个迭代函数f、表t0,因此可以使用:

1
2
3
for i,v in ipairs(t) do
-- do something
end

这种格式来迭代(1,t[1]), (2,t[2]) ...这样成对的值,直到没有更多的整数索引。

1
function pairs(t) end

传入参数是一个表,该函数返回三个值,依次是函数next、表tnil,因此可以使用:

1
2
3
for k,v in pairs(t) do
-- do something
end

这种格式来迭代表中所有的键值对。

函数next

1
function next(table, index) end

可用next遍历整个表,第一个参数是表,第二个参数是表内的一个索引。next会返回表的下一个索引和关联的值。当第二个参数是nil时,next会返回一个初始索引和其关联的值。当第二个参数是表的最后一个索引时,或者当表是空表而第二个参数是nil时,next会返回nil。如果第二个参数空缺,则认为是nil,此时你可以用next(t)来判断一个表是否为空表。表内索引值的迭代顺序是不确定的,即便是数字型索引。如果在使用next遍历过程中给遍历中的表增加新的值,将会出现为定义行为。但是你可以修改甚至清除当前已有的值。

使用next方法,则pairs函数可理解为:

1
2
3
function pair(t)
return next, t
end

raw方法

使用rawgetrawset避免触发__index__newindex方法:

1
2
3
4
5
6
7
8
9
10
11
12
local t = {a = "a"; b = "b"; c = "c"}
local mt = { __index = {d = "metatable-value d"}; __newindex = function(t,k,v) print("cant set value") end}
setmetatable(t,mt)

print(t["c"])
print(rawget(t,"c"))
print(t["d"])
print(rawget(t,"d"))
t["e"]= "e"
print(t["e"])
rawset(t,"f","raw-set f")
print(t["f"])

将会输出:

1
2
3
4
5
6
7
c
c
metatable-value d
nil
cant set value
nil
raw-set f

除此之外还有rawequal,避免触发__eq方法:

1
2
3
4
5
6
7
8
9
10
local eq = function(t1,t2)
return t1.name == t2.name
end
local t1 = setmetatable({name = "haha"},{__eq = eq})
local t2 = setmetatable({name = "haha"},{__eq = eq})

print("t1 == t2 -> " .. tostring(t1 == t2))
print("rawequal(t1,t2) -> " .. tostring(rawequal(t1,t2)))
print("t1 == t1 -> " .. tostring(t1 == t1))
print("rawequal(t1,t1) -> " .. tostring(rawequal(t1,t1)))

将会输出:

1
2
3
4
t1 == t2  -> true
rawequal(t1,t2) -> false
t1 == t1 -> true
rawequal(t1,t1) -> true

package相关

查找路径

require某个脚本被告知module not found或者不确定脚本路径从哪一级写起时,可以使用以下方法显示当前的查找路径。

1
print(package.path)

package.path中记录了所有的包含路径。如果要增加某个路径,只需像拼接字符串那样接在后边即可:

1
package.path = package.path .. ";/SomeFolder/?.lua"

模块重新加载

对于已经加载的Lua模块,如果再次require会直接得到第一次加载时的内容。加载过的模块都可以在
package.loadedpackage.preloaded 中找到。可以手动将其置为nil,然后再重新require。使用这种方法,可以在运行时不重启程序,修改Lua脚本并应用修改内容,用来调试Lua代码非常方便。

文件夹m下有文件mod1.lua 内容如下:

1
2
3
4
5
func = function()
print("old func")
end

print("mod1 is required")

测试代码,可以在命令行测试:

1
2
3
4
5
6
7
8
9
10
11
12
require ("m.mod1")
func()
print(tostring(package.loaded["m.mod1"]))
require ("m.mod1")

package.loaded["m.mod1"] = nil
print(tostring(package.loaded["m.mod1"]))

-- 此时修改mod1内容,将`old func`改为`new func`

require ("m.mod1")
func()

这种操作只会影响到下次require时执行的动作,但不会影响到已经加载到内存中的内容。

环境

默认情况下,全局变量都保存在一个名为_G的表里,也可以通过遍历_G获取全部的全局变量/函数,如:

1
2
3
4
5
6
7
8
t = {"a","b","c"}
print(tostring(t))
print(tostring(_G["t"]))
print(tostring(t[2]))
print(tostring(_G["t"][2]))
for k,v in pairs(_G) do
print( "_G[" ..tostring(k) .. "] = " .. tostring(v))
end

setfenv和getfenv

这个_G就是一个默认的函数环境,有时候我们可能会有一些需求要使用其它的环境而非默认的_G,在5.1版本的lua中我们可以使用setfenvgetfenv来设置和获取环境。

setfenv接收两个参数,第一个参数为一个函数或者一个数字,第二个参数为设置的目标环境表。当第一个参数为函数时,表示设置该函数的环境,若第一个参数为数字(1、2、3…),1表示当前函数,2表示更外一层即调用当前函数的函数,以此类推。

1
2
3
4
5
6
7
8
9
local newEnv = {}
local prt = print
newEnv.print = function(arg)
prt("[new env print] " .. arg)
end
setfenv(1,newEnv)


print("a")

_ENV

5.2及更高版本的lua废弃了setfenvgetfenv,取而代之使用_ENV来设置环境,如:

1
2
3
4
5
6
7
8
9
local newEnv = {}
local prt = print
newEnv.print = function(arg)
prt("[new env print] " .. arg)
end
_ENV = newEnv


print("a")

print是新的环境中的函数。

沙盒环境

制作沙盒环境,只能访问到希望访问到的函数,并且对全局变量的修改也都是在临时的新环境中进行,上边的例子就是一种应用,将希望在新环境中使用的函数(全局的print),使用upvalue的形式(prt)引用到新的环境中newEnv.print

如果在新环境中使用_G的函数,另一种方法是:

1
2
3
4
5
6
local newEnv = {_G = _G}

_ENV = newEnv

_G.print("a")
_G.print(_G.tostring(3+3))

或者直接将_G放到元表中,如果当前环境中未定义某个方法,则到_G中去找:

1
2
3
4
5
6
7
8
9
10
11
12
local newEnv = setmetatable({},{__index = _G})

_ENV = newEnv

local prt = print

print = function(arg)
return prt("[new env print] " .. arg)
end

print("a")
print(tostring(3+3))

print是新的环境中的函数,而tostring是原来的函数。

封装模块

当编写一些模块时,模块内的一些方法或者成员我们希望它们是私有的,即不被外部访问到,通常在模块内可以将他们声明为local,但是这么做需要严格控制其定义的顺序。另一种方法是在模块内部使用一个新的环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local mod = {}
local _G = _G
local env = setmetatable({},{__index = _G})

_ENV = env


function GlobalPrintAdd(a,b)
return print("Global: " .. PrivateAdd(a,b))
end

function ModulePrintAdd(a,b)
return print("Module: " .. PrivateAdd(a,b))
end

function PrivateAdd(a,b)
return a .. " + " .. b .. " = " .. a + b
end

mod.PrintAdd = ModulePrintAdd
_G.PrintAdd = GlobalPrintAdd

return mod

加载该模块后,可以调用模块方法及全局的方法,但是不能调用到私有的方法:

1
2
3
4
5
6
local m = require "m"

m.PrintAdd(8,8)
PrintAdd(7,7)
-- m.PrivateAdd(3,4)
-- PrivateAdd(3,4)

string模块

Lua中最常用的模块之一,用于字符串相关的操作,这里记录其中几个函数的使用:

string.dump

可以将一个函数序列化成字符串,然后在需要的时候将其加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local GetSum = function(...)
local ret = 0
for _,v in ipairs({...}) do
ret = ret + v
end
return ret;
end

print(GetSum(1,2,3,4,5))

local str =string.dump(GetSum)

local load = load or loadstring
local newFunc = load(str)

print(newFunc(1,3,5,7,9))

string.gsub

string.gsub(s, pattern, repl [,m])。简单来说,就是字符串匹配替换,将字符串内的某些内容替换成希望的内容,如:

1
2
3
4
5
6
7
8
local str = "hello world hello game hello"
print(str)
local newStr = string.gsub(str,"hello","bye",1)
print(newStr)
newStr = string.gsub(str,"hello","bye",2)
print(newStr)
newStr = string.gsub(str,"hello","bye")
print(newStr)

但是gsub的功力远不限于此,首先第二个参数pattern可以是正则表达式,正则表达式中用%来转义,并且以%n的形式在repl中取到捕获组:

1
2
3
4
local str = "hello world hello game hello"
print(str)
local newStr = string.gsub(str,"hello (%w+)","hello [%1]")
print(newStr) -- hello [world] hello [game] hello

第三个参数更加灵活,可以是字符串、表或者函数:

  • 如果是字符串,则直接替换,并且是以%n这样的形式来获取匹配捕获的内容。

  • 如果是表,则以匹配到的内容为key,到表里去取值并返回取到的值:

    1
    2
    3
    4
    5
    local 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
    7
    local 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
2
3
4
5
6
function SplitString(str, sep)
local sep, fields = sep or "\t", {}
local pattern = string.format("([^%s]+)", sep)
string.gsub(str, pattern, function(c) table.insert(fields,c) end)
return fields
end

string.gmatch

string.gmatch(s, pattern),此函数返回的是一个迭代器函数,每次调用时即会返回在s中用pattern匹配到的下一个字符串,直到再没有更多的匹配时返回nil

这个gmatch返回的是一个迭代器函数,有点像前边说到的ipairspairs此类。以下是一个示例:

1
2
3
4
local str = "hello world hello game hello"
for v in string.gmatch(str,"%w+") do
print("[" .. v .. "]")
end

时间相关计算

os.time

可以获取一个时间戳,参数为空时返回当前时间戳,参数为一个表时,根据表内指定的时间返回对应时间戳:

1
2
3
print(os.time())
print(os.time({year =2018, month = 6, day =23, hour =12, min =00, sec = 00, isdst = false}))
print(os.time({year =2018, month = 6, day =23}))

表内yearmonthday为必填字段,hourminsec为选填字段,缺省为12:00:00isdst表示是否夏时令也是选填字段,缺省为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
2
print(os.date("%Y-%m-%d %H:%M:%S"))  -- 2018-06-23 13:25:54
print(os.date("!%Y-%m-%d %H:%M:%S")) -- 2018-06-23 05:25:54

如果使用了*t,那么输出的不再是一个字符串,而是一个表:

1
2
3
4
local t = os.date("*t")
for k,v in pairs(t) do
print(k .. " = " .. tostring(v))
end

将会输出

1
2
3
4
5
6
7
8
9
yday = 174
year = 2018
month = 6
sec = 15
day = 23
isdst = false
wday = 7
hour = 13
min = 28

可以借助difftime来获取本地的时区

1
2
3
local now = os.time()
local region = os.difftime(now, os.time(os.date("!*t", now)))/3600
print("region = " .. region) -- region = 8

log输出调试信息

输出调用栈

最简单实用的,输出当前调用栈:

1
print(debug.traceback())

后边再给一个例子。

debug.getinfo

获取更多的调试信息,getInfo需要传入一个整数表示函数调用级别,0表示自身(getinfo),1表示直接调用者,2表示更上一层的调用函数,以此类推,以下是一个示例,func5调用了func4,数字递减直到func2调用了func1,在func1的内部输出调试信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local printDebugInfo = function(level)
local info = debug.getinfo(level)
print(string.format("file=%s function=%s line=%d",info.source,info.name,info.currentline))
end

local function func1()
print(debug.traceback())
printDebugInfo(0)
printDebugInfo(1)
printDebugInfo(2)
printDebugInfo(3)
end
local function func2()
func1()
end
local function func3()
func2()
end
local function func4()
func3()
end
local function func5()
func4()
end


func5()

输出内容为:

1
2
3
4
5
6
7
8
9
10
11
12
stack traceback:
test.lua:7: in function 'func1'
test.lua:14: in function 'func2'
test.lua:17: in function 'func3'
test.lua:20: in function 'func4'
test.lua:23: in function 'func5'
test.lua:27: in main chunk
[C]: in ?
file==[C] function=getinfo line=-1
file=@test.lua function=printDebugInfo line=2
file=@test.lua function=func1 line=10
file=@test.lua function=func2 line=14

错误处理

error和assert

编写一些底层函数或者工具方法时,需要增加一些错误提示或者断言,以使调用者在错误地使用这些方法后可以很快知道出了什么问题,可以使用error来实现此功能:

1
2
3
4
5
6
7
local funcAdd = function(a,b)
if type(a)~= "number" then error("a is not a number") end
if type(b)~= "number" then error("b is not a number") end
return a + b
end

funcAdd(1,"")

将会输出:

1
2
3
4
5
6
lua: test.lua:3: b is not a number
stack traceback:
[C]: in function 'error'
test.lua:3: in function 'funcAdd'
test.lua:7: in main chunk
[C]: in ?

assert其实可以更简化error。其语法其它编程语言中的断言很相似,刚才的funcAdd可以简化为:

1
2
3
4
5
local funcAdd = function(a,b)
assert(type(a) == "number", "a is not a number")
assert(type(b) == "number", "b is not a number")
return a + b
end

输出的内容是相同的。

pcall

Lua中的异常处理机制,类似于其它语言中的try...catchpcall以保护模式调用函数,并返回函数是否成功的调用,以下是一个示例:

1
2
3
4
5
6
7
8
9
10
local funcAdd = function(a,b)
print("Calling funcAdd(" .. tostring(a) .. "," .. tostring(b).. ") ...")
return a + b
end

local state, ret
state, ret = pcall(funcAdd,3,6)
print("state = [" .. tostring(state) .. "] ret = [" .. ret .. "]")
state, ret = pcall(funcAdd,8)
print("state = [" .. tostring(state) .. "] ret = [" .. ret .. "]")

state用来记录函数调用是否成功,ret用来记录函数的返回值。将会输出

1
2
3
4
Calling funcAdd(3,6) ...
state = [true] ret = [9]
Calling funcAdd(8,nil) ...
state = [false] ret = [test.lua:3: attempt to perform arithmetic on local 'b' (a nil value)]

xpcall

xpcallpcall类似,比pcall多传入一个错误处理函数,通常可以用来打印出调用栈:

1
2
3
4
5
6
7
8
9
10
11
local funcAdd = function(a,b)
print("Calling funcAdd(" .. tostring(a) .. "," .. tostring(b).. ") ...")
return a + b
end

local errHandler = function()
print(debug.traceback())
end


state, ret = xpcall(funcAdd,errHandler,8)

输出的内容如下:

1
2
3
4
5
6
7
Calling funcAdd(8,nil) ...
stack traceback:
test.lua:7: in function '__add'
test.lua:3: in function <test.lua:1>
[C]: in function 'xpcall'
test.lua:11: in main chunk
[C]: in ?

弱引用表

Lua的GC机制,当变量有任何引用时,就不会被清理掉。但是在很多情况下,一些变量我们不再使用了,但是因为有引用,Lua的GC不会将其识别为可清理的垃圾。使用弱引用表可以解决此问题,弱引用表可以设置键或者值对变量弱引用,通过其元表的__mode字段来设置,__mode的值是一个字符串,若该字符串包含k,则该表的键为弱引用,若该字符串包含v,则该表的值为弱引用,若同时包含kv,则键和值都是弱引用,可以看一下的一组示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
local t1 = {}
local t2 = setmetatable({},{__mode = "k"})
local t3 = setmetatable({},{__mode = "v"})
local t4 = setmetatable({},{__mode = "kv"})

local kt = {"key"};
t1[kt],t2[kt],t3[kt],t4[kt] = {"v1"},{"v2"},{"v3"},{"v4"}
local vt = {"value"};
t1[{"k1"}],t2[{"k2"}],t3[{"k3"}],t4[{"k4"}] = vt,vt,vt,vt

collectgarbage()


for k,v in pairs(t1) do
print("t1[" .. k[1] .. "] = " .. v[1])
end
print(string.rep("-",5))
for k,v in pairs(t2) do
print("t2[" .. k[1] .. "] = " .. v[1])
end
print(string.rep("-",5))
for k,v in pairs(t3) do
print("t3[" .. k[1] .. "] = " .. v[1])
end
print(string.rep("-",5))
for k,v in pairs(t4) do
print("t4[" .. k[1] .. "] = " .. v[1])
end

将会输出:

1
2
3
4
t1[key] = v1
t1[k1] = value
t2[key] = v2
t3[k3] = value

上边的四个表,t1是普通的表,t2t3t4分别是键弱引用、值弱引用、键和值都弱引用的表。当执行了GC之后,ktvt会被保留,而其余的{"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