概述
如果要更全面的学习Lua语言,可以查阅下面文档:
- Lua程序设计(PIL):lua作者写的一本书,是学习lua的不二之选。
- Lua参考手册5.3版:lua的使用手册。
基础数据类型
lua一共有8种基本的数据类型:nil, string, number, boolean, table, function, thread, userdata
。
其中nil表示没有值,和其他类型都不相等,在条件判断中只有nil和false表示否定。
string是字符串类型,它本质上是一个字节序列,也就是它说可以存储任意数据,5.3以后增加了utf8的函数库,使用它可以对utf8字符串作正确的处理。
number代表数值类型,在5.3以前是没有整型的,5.3以后加上了整型作为number的子类型,使用math.type
判断一个数值是整型还是浮点数。
boolean代表布尔值,很早期的lua版本中并没有boolean类型,boolean类型的出现,大概是为了解决table中空值的问题。
table是唯一的数据结构
table是lua唯一的数据结构,其他结构都可以用table实现,它不仅可以用数值作索引,也可以用其他类型(比如字符串,函数,甚至另一个table),lua内部用一个数组和一个哈希表来实现table。这使得table非常强大,即可以用作字典,也可以用作数组,配合元表机制还可以模拟面向对象。lua的很多基础设施,比如模块,全局变量,元表,都是基于table实现的。在语言层面它是如此简洁,可又能实现无比强大的功能。
table的简单声明:
-- 当成字典使用
local t = {
a = 1,
b = true,
c = "haha",
}
-- 当成数组使用
local t2 = {1, "aa", false}
table甚至可以同时表示字典和数组:
local t3 = {
1, 2, 3,
a = "aaa",
b = "bbb",
}
实际上他们可以使用通用的格式来表示 :
local t3 = {
[1] = 1,
[2] = 2,
[3] = 3,
["a"] = "aaa",
["b"] = "bbb",
}
但在实践中,我们最好不混用字典和数组,这常常会引发混乱的问题。而从设计的角度看,我认为它违反了单一职责原则。比如空Table就且有二义性,如果我们有一个需求,要将空Table转换成JSON格式,此时JSON库就不知道如何处理了,因为它不知道要转成空数组还是空对象,cjson默认会转成对象,如果要转成数组,需要用另外的API。
table将key的值设为nil,它的真实含义是删除掉这个key,这和其他脚本很不一样,也可能引发一些问题,比如看下面例子:
local t = {1, 2, nil, 4}
print(#t) ---> 4
for _, v in ipairs(t) do print(v) end ---> 1 2
for _, v in pairs(t) do print(v) end ---> 1 2 4
通过ipairs遍历t,只能遍历到2,而通过pairs遍历t,则不会输出nil,这说明第3个元素是不存在的。要正确遍历t,也许只能这样写:
for i = 1, #t do print(t[i]) end ---> 1 2 nil 4
但是,我这里说也许,因为出现nil的情况,#是未定义的,再看下面的例子:
local t = {1, 2, nil, nil, 5}
print(#t) --> 5
t = {[1]=1, [2]=2, [3]=nil, [4]=nil, [5]=4}
print(#t) --> 2
两种表达方式,得到的结果不一样,所以在table中不能使用nil,应该使用false代替。有时候简洁也许并不都是好事,如果能分离出一个数组类型,很多问题就自然解决了。
函数
Lua函数的声明大概是这样的:
function fun(a, b, c)
return a + b, c
end
lua函数支持多返回值,也支持可变参数,结合多返回值,可以实现一个print函数:
function myprint(...)
local str = table.concat({...}, "t") .. 'n'
io.stdout:write(str)
io.stdout:flush()
end
function myfun()
return 1, 2, 3
end
myprint(myfun()) --> 1 2 3
调用函数时不用严格匹配参数数量,如果少传参数,剩余的参数默认为nil, 如果多传则多传的参数会丢弃。这可以模拟默认参数的实现:
local function test(a, b, c)
if not c then c = 1 end -- 如果c为nil,则c=1
return a + b, c
end
print(test(1, 2)) --> 3 1 这里只传了2个参数
lua可以使用pcall, xpcall来实现错误的捕获,在pcall中执行的函数如果出错,会返回false,后面带一个错误消息,看下面的代码:
local function test(a)
if a == 2 then
error("test error")
end
return true
end
local ok, ret = pcall(test, 1) --> true true
local ok, ret = pcall(test, 2) --> false test.lua:5: test error
pcall返回的第1个值表明test函数是否成功执行,如果为true,则后面的返回值就是test函数的返回值;如果为false,后面的值是一个错误消息。但是pcall有时候并不能满足要求,比如我们想知道错误是在哪里发生的,那么就要用xpcall来实现:
local function msghander(msg)
print(msg)
print(debug.traceback())
end
local ok, ret = xpcall(test, msghander, 1) --> true true
local ok, ret = xpcall(test, msghander, 2) --> false nil
使用debug.traceback()输出调用堆栈如下:
test.lua:5: test error
stack traceback:
test.lua:11: in function <test.lua:9>
[C]: in function 'error'
test.lua:5: in function <test.lua:3>
[C]: in function 'xpcall'
test.lua:14: in main chunk
[C]: in ?
这样就能清楚地知道问题出在哪里。
内部函数里可以引用外部函数的局部变量,这些引用被称为upvalue,而Lua的函数也可以叫闭包,我们为了便于理解还是叫函数。闭包和upvalue是非常重要的特性,后面我们将实现一个函数级别的重载机制,其中最重要的就是对于upvalue的处理。
作为一个典型的应用,假设要创建一个对象,对外只提供有限的访问接口,而对象内部的数据不能直接被修改,那么我们可以这样写:
local function new_object()
local obj = { -- 这就是要创建的对象
_data1 = 1, -- 假设这是内部数据
_data2 = 2, -- 这是外部可修改的数据
}
return { -- 这是返回的接口table
get_data2 = function() return obj._data2 end,
set_data2 = function(value) obj._data2 = value end,
}
end
local obj_inteface = new_object()
obj_inteface.set_data2(100)
print(obj_inteface.get_data2()) --> 100
调用new_object后得到的实际是一个代理table,这个表暴露出一些接口函数,而这些函数又引用着new_object内部的obj变量,因此这些函数可以对obj进行访问。
元表
元表使得lua更为强大,它其实也是一个普通的table,但可以改变表的行为,给一个table设置元表,当对这个table执行特定操作时,如果元表存在对应的元方法,则会调用该元方法。元表大概是这样设置的:
local t = {} -- 表
local mt = {} -- 元表
setmetatable(t, mt) -- 将mt设置为t的元表
现在mt就可以通过元方法影响t的行为了,元方法有很多,但有些并不常用,这里只列出我认为比较重要的:
__tostring
如果t的元表存在tostring元方法,调用tostring(t)时,会调用元表的tostring方法,这样我们就可以对t进行序列化,下面是一个例子:
local mt = {
__tostring = function(t)
return string.format("a=%s, b=%s", t.a, t.b)
end
}
local t = {a = 1, b = 2}
setmetatable(t, mt)
print(tostring(t)) --> a=1,b=2
实际上只要print(t)就可以了,因为print内部会自动调用tostring(t)
__pairs
当调用pairs(t)时,如果元表存在__pairs,就会调用该元方法并传入t。这可以实现对表的特殊遍历,比如下面的表:
local t = {a = 1, b = "s", c = "ss", d = 1}
我们想遍历出值是number类型的元素,这时就可以使用__pairs来实现,在实现之前我们先对泛型for循环作一个介绍,pairs返回3个变量:迭代器函数(比如next),表,当前索引。for循环从pairs得到这3个变量后,会调用迭代函数,如:next(t, index),一开始index为nil,这样next就返回第一个index和value。 接着for循环不断的调用next并传入上一次调用得到的index,直到next返回nil为止。
现在就可以看看__pairs应该怎么实现了,具体代码如下:
local mt = {
__pairs = function(t)
local function itr(t, idx)
local v
while true do
idx, v = next(t, idx)
if not idx then
return nil
elseif type(v) == 'number' then
return idx, v
end
end
end
return itr, t, nil
end
}
local t = {a = 1, b = "s", c = "ss", d = 1}
setmetatable(t, mt)
for k, v in pairs(t) do
print(k, v)
end
__len
如果t是一个数组,用#t可以取得该数组的长度, 但如果是字典就不行了,必须用__len元方法:
local mt = {
__len = function(t)
local n = 0
for k, v in pairs(t) do
n = n + 1
end
return n
end
}
local t = {a = 1, b = "s", c = "ss", d = 1}
setmetatable(t, mt)
print(#t) --> 4
index, newindex
这两个元方法用于对table的读写控制。
读表中key关联的值,如果key不存在,会调用元表的index。index可以是一个函数__index(t, k);也可以是另一个表,此时会直接读取该表的key值。如果不想调用t[k]时触发元方法,可以使用rawget(t, k)。
要设置表中key关联的值,如果key不存,会调用元表的newindex。同样newindex可以是一个函数__newindex(t, k, v);也可以是另一张表,此时会直接向该表的key设置值。如果不想t[k]=v时触发元方法,可以使用rawset(t, k, v)。
这两个元方法,可用于实现类似面向对象的机制,下一章我们会详述如何使用它们来实现面向对象。
__call
这个元方法可使非函数对象像函数一样调用,比如一个表t,我们这样写:t("hi"),如果它的元表存在__call函数,则会触发调用:
__call = function(t, a1) -- a1 == "hi"
end
后面我们实现的面向对象也会用到这个元方法,用它来实现类似Python创建对象的写法:
local obj = SomeClass()
后记
Lua的细节远不止于此,上面只是对一些要点作一些浅析,建议详细阅读前面给出的文档。