一、前言
1、lua提供的是自动内存管理机制
2、lua可以同时运行在windows和UNIX平台上
3、lua用户可以分为三大类:使用嵌入在某个应用程序中的lua的用户、使用lua解释器程序的用户、同时使用Lua和c的用户
4、table是lua中唯一的一种数据结构
5、lua的官方网站:http://www.lua.org
6、对于lua来说,this和that等价
7、lua没有提供class关键字,也没有显示继承、virtual、struct、name space、原生数组,但是提供了模拟这些的数据结构table
二、开始
1、输出hello
print("hello world"),运行时使用lua hello.lua
2、阶乘函数
function fact(n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
print("enter a number:")
a=io.read("*number")
print(fact(a))
但是像下面这么写是错误的:
local fact = function(n)
if n == 0 then return 1
else return n*fact(n - 1)
end
end
这是由于当Lua编译到函数体调用fact(n - 1)时,由于局部的fact尚未定义完毕,因此这句表达式其实是调用了一个全局的fact,而非此函数自身,但是并未存在一个全局的fact,所以会出错。解决办法是:可以先定义一个局部变量,然后再定义函数本身
local fact
fact = function(n)
if n == 0 then return 1
else return n*fact(n - 1)
end
end
现在当编译到fact(n - 1)时,由于局部变量覆盖全局变量的原则,调用的就是局部变量fact了,即使在函数定义时,这个局部变量的值尚未完成定义,但之后在函数执行时,fact则肯定已经拥有了正确的值
3、退出
os.exit()
4、交互模式
lua -i prog.lua
会先运行prog中的程序块,然后再进入交互模式
5、哑变量
lua中以一个下划线开头并跟着一个或多个大写字母的变量作为哑变量
6、lua中的关键字
and break do else elseif end false for function if in local nil not or repeat return then true until while
7、行注释和块注释
-- print(10)
--[[
print(10)
--]]
8、解释器程序
(1)
#!/usr/local/bin/lua
或
#!/usr/bin/env lua
lua文件的第一行这么写,则加载文件时,解释器将忽略这一行,这是为了方便在UNIX系统中将lua作为一种脚本解释器来使用
(2)解释器用法
lua [选型参数][脚本[参数]]
参数-e可以直接在命令中输入代码
lua -e "print(math.sin(12))"
则会得到运行结果
参数-l用于加载库文件
参数-i表示运行完其他命令行参数后进入交互模式
lua -i -l a -e "x=10"
先加载库文件a,然后执行赋值语句,最后显示一个交互模式的命令提示符
在解释器执行参数前,会先查找一个名为LUA_INIT的环境变量,如果找到,并且其内容为@文件名,那么解释器会先执行这个文件,如果LUA_INIT没有以@开头,那么解释器就假设变量内容为lua代码,并运行此代码
全局变量arg会检索脚本的启动参数
lua 脚本 a b c
解释器在运行脚本前,会用所有的命令行参数创建一个名为arg的table,脚本名称位于索引0上,第一个参数a位于索引1上,在脚本文件之前的选项参数位于负数索引上
lua -e "sin=math.sin" script a b
则:
arg[-3]为"lua"
arg[-2]为"-e"
arg[-1]为“sin=math.sin”
arg[0]为"script"
arg[1]为"a"
arg[2]为"b"
通常脚本只会使用正数索引
变长参数语法:表达式...(3个点)表示所有传递给脚本的参数
三、类型与值
1、八种基本类型
nil number string boolean function thread userdata table
type函数可以返回某个变量的值(type函数本身返回的值是string类型)
变量可以在运行的过程中存储不同类型的值,这时它所属的类型也将改变(但是这样可能造成混乱,所以尽量还是不要这么做)
2、nil
变量在被赋值前的默认值,在使用过程中对一个全局变量赋值为nil相当于删除它
3、boolean
lua中false和nil视为假,其他视为真(数字零和空字符串也都视为真)
10 == ”10“为false
tostring(10) == "10"为true
nil只与其自身相等
对于table、userdata和function,lua是作引用比较的,即只有当它们引用同一个对象时,才认为它们相等
4、number
表示实数,因为没有整数类型,number类型变量可以表示任何32位整数
5、string
采用8位编码,不可变
a = "one string"
b = string.gsub(a,"one","another")
print(a) --输出one string
print(b)-- 输出another string
转义符
\a:响铃
\b:退格
\f:提供表格
\n:换行
\r:回车
\t:水平tab
\v:垂直tab
\\:反斜杠
\":双引号
\':单引号
lua提供了运行时数字与字符串的自动转换
print("10" + 1) -- 输出11
print("10 + 1") -- 输出10 + 1
print("5" * "2") -- 输出10
print("hello" + 1) -- 错误
pring(10 .. 20) --输出1020,因为..是字符串连接操作符(有空格)
字符串和数字之间的转换可以用tonumber和tostring来实现,如果转换失败则会返回nil
字符串前放#可以获得字符串的长度
a = "hello"
print(#a)输出5
6、table
可以通过整数来索引,也可以通过字符串或其他类型值(除nil)来索引
table在lua中是个对象,程序仅持有一个对他们的引用
table通过{}来进行创建(不可以通过[])
a = {}
a["x"] = 10
b = a
pring(b["x"])输出10
b["x"] = 20
pring(a["x"])输出20
a = nil -- 现在只有b还在引用table
b = nil -- 再也没有对table的引用了,lua的垃圾收集器最终会删除该table,并复用它的内存
a.x 等价于a["x"]
但是前者暗示a有一组固定的、预定义的key。后者暗示了会以任何字符串作为key
lua中的数组通常以1作为索引的起始值(而不是0),长度操作符#用于返回一个数组或线性表的最后一个索引值(相当于length)
for i = 1,#a do
print(a[i])
end
上面代码为输出数组a中的所有数据
print(a[#a])输出数组a的最后一个值
a[#a] = nil -- 删除最后一个值
a[#a] = x -- 用x替换数组的最后一个值
a[#a + 1] = v -- 将v添加到数组末尾
下面代码用于读取一个文件的前10行
a = {}
for i=1,10 do
a[#a + 1] = io.read()
end
返回一个table的最大索引数
a = {}
a[1000] = 1
print(table.maxn(a))输出1000(但是其实a[2]、a[3]等均为nil,所以这个数组是有"间隙"的),因此1000并不是该数组的长度
上面是普通风格的table,下面介绍几种特殊风格的table
(1)数组风格的table
days = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Firday","Saturday"}
会将days[1]初始化为字符串"Sunday"
如果想让数组的下标从0开始,那么可以这么写:
days = { [0] = "Sunday","Monday","Tuesday","Wednesday","Thursday","Firday","Saturday"}
现在Sunday的索引就为0了,Monday索引为1(由于lua中数组的索引默认是从1开始的,所以并不推荐使用这种方式来让数组的下标从0开始)
(2)记录风格的table
a = {x=10,y=20}等价于a = {};a.x=10;a.y=20
(3)数组风格和记录风格结合的table
polyline = {color="blue",thickness=2,npoints=4,
{x=0,y=0},
{x=-10,y=0},
{x=-10,y=1},
{x=0,y=1}
}
print(polyline[1].x)输出0
print(polyline[2].x)输出-10
或是用分号代替逗号,来做明显的区分
{x=10,y=45;"one","two","three"}
(4)方括号风格的table(数组风格的table不能使用负数作为索引,记录风格的table不能用运算符作为key),这种风格更通用
opnames={["+"] = "add",["-"] = "sub",["*"] = "mul",["/"] = "div"}
a = {[20+0] = "haha",[20+1] = "hehe"}
print(a[21])输出hehe
7、function
同javascript一样,lua中的function也可以保存在变量中,可以通过参数传递给其他函数,也可以作为函数返回值
8、userdata
可以将任意的c语言数据存储到lua中
9、thread
线程,叫做协同程序(coroutine)
四、表达式
1、指数操作符^
x^0.5将计算x的平方根
2、取模操作符%
a%b == a - floor(a/b)*b
与java不同的是计算结果的符号永远与第二个参数相同
对于实数来说,x%1是其小数部分,x-x%1是其整数部分,x-x%0.01是x精确到小数点后两位的结果
3、不等性关系操作符~=
lua中如果两个值具有不同的类型,就认为是不相等的
4、逻辑操作符and、or和not
and:如果它的第一个操作数为假,就返回第一个操作数,不然返回第二个操作数
or:如果第一个操作数为真,就返回第一个操作数,不然返回第二个操作数
x = x or v等价于if not x then x = v end
not:永远只返回true或false
5、字符串连接符..
如果其任意一个操作数是数字的话,lua会将这个数字转换成一个字符串
连接符会产生一个新的string对象,而不会对两个操作数做修改
五、语句
1、多重赋值
a,b = 10,15
x,y = y,x -- 交换x与y
若值的个数少于变量的个数,那么多余的变量会变成nil;如果值的个数多于变量的个数,那么多余的值被丢弃
a,b = f() 这种情况是函数f将返回两个值,a接收了第一个,b接收了第二个
2、局部变量与块
局部变量使用local来创建,使用局部变量时在需要的时候才声明变量,可以使这个变量在初始化时刻就拥有一个有意义的初值,此外,缩短变量的作用域有助于提高代码的可读性。所以没有必要把局部变量的声明统一放在一个程序段的起始位置
3、控制结构
所有的控制结构都有一个显示的终止符:if、for和while以end作为结尾,repeat以until作为结尾。(不支持switch)
(1)if then elseif then else end
(2)while do end
(3)repeat
repeat
line = io.read()
until line~=""
print(line)
(4)for
数字型for和泛型for
数字型for:
for var = exp1,exp2,exp3 do
<执行体>
end
var 从exp1变化到exp2,每次以exp3为步长递增,exp3可选,若不指定的话,lua会将步长默认为1(如果想递减,那么exp3可为负值;如果不想给循环设置上限,可以将exp2设为math.huge)
for循环的三个表达式是在循环开始前一次性求值的;for中的控制变量会被自动的声明为for语句中的局部变量,并且仅在循环体内可见,控制变量在循环结束后就不存在了;不要在循环过程中修改控制变量的值,否则可能导致不可预知的效果
泛型for:
for i,v in ipairs(a) do
print(v)
end
提供了ipairs,用于遍历数组的迭代器函数,i会被赋予一个索引值,同时v被赋予一个对应于该索引的数组元素值
for k in pairs(t) do
print(k)
end
用于打印一个table中的key(pairs用于迭代table元素)
此外,迭代文件中的每行(io.lines)、迭代字符串中单词(string.gmatch)
(5)break
break用于结束一个循环,它只会跳出包含它的那个内部循环(for、repeat、while),而不会改变外层的循环,在执行了break后,程序会在那个被跳出的循环之后继续执行。
break和return只能是一个块的最后一条语句或是end、else、until前的一条语句,因为那些位于return或break之后的语句将无法执行到
六、函数
使用函数的时候需要将参数放到一对圆括号中,除了一种情况,那就是一个函数如果只有一个参数,并且此参数是一个字面字符串或table构造式,那么圆括号是可有可无的
print "Hello world" 等价于print("Hello world")
dofile "a.lua" 等价于 dofile('a.lua')
type{}等价于type({})
使用冒号操作符调用函数:
o.foo(o,x)的另一种写法是o:foo(x)。冒号操作符使调用o.foo时将o隐含地作为函数的第一个参数
实参多余形参,舍弃多余实参;实参不足,多余形参初始化为nil
1、函数返回多个值
只需要在return关键字后列出所有的返回值即可,返回值之间用逗号隔开,注意不能使用括号括住所有的返回值,使用括号括住所有的值将只返回一个值
特殊函数unpack,接受一个数组作为参数,并从下标1开始返回该数组的所有元素
print(unpack{10,20,30}) 输出10 20 30
a,b = unpack{10,20,30} 则a=10,b=20而30被丢弃
2、变长参数(使用...)
例子:
function add(...)
local s = 0
for i,v in ipairs{...} do
s = s + v
end
return s
end
print(add(3,4,10,25,12)) 输出54
上例中{...}表示一个由所有变长参数构成的数组,而add遍历了该数组,并累加了每个元素
local a,b = ...
上例变长参数列表中的前两个值初始化了两个局部变量a,b
当有固定参数和变长参数结合使用的时候,见下例
fuction testfun(fmt, ...)
即在声明方法的时候,把固定的参数fmt放在变长的参数之前,如果调用方法的时候没有传入任何的参数,那么默认方法中的fmt为nil
如果方法中只需要使用传入的...中的某一些参数,那么可以使用select,看下例:
for i=1,select('#', ...) do
local arg = select(i, ...) --得到第i个参数
<循环体>
end
上例中如果select函数的第一个参数为数字n,那么select函数返回的是...中的第n个参数;如果第一个参数不为数字n,那么只能为#,select会返回...的参数个数(包括实参nil在内)
3、具名实参
默认状态下实参和形参是按照顺序一一对应的,但在lua中还可以通过名称来指定实参和形参的对应关系
例如:
function rename(arg)
return os.rename(arg.old,arg.new)
end
调用的时候:
rename({old = "temp.lua",new = "temp1.lua"})
即传入一个实参,该实参是个对象,对象中指定了key,然后在方法中通过传入的对象的key来使用值
4、深入理解lua中的函数
(1)函数与其他值一样都是匿名的,当讨论一个函数名时,实际上是在讨论一个持有某函数的变量
(2)
function foo(x) return 1 end
其实就是
foo = function(x) return 1 end
5、lua中支持闭合函数(一个闭合函数就是一种可以访问其外部嵌套环境中的局部变量的函数,这些变量可用于在成功调用之间保持状态值)
使用闭合函数可以为代码创建一个安全的运行环境,即所谓的“沙盒”。当执行一些未受信任的代码时就需要一个安全的运行环境,例如在服务器中执行那些从internet上接收到的代码。例如如果要限制一个程序访问文件的话,只需要使用闭包函数来重定义函数io.open即可
do
local oldOpen = io.open
local access_Ok = fuction(filename,mode)
<检查访问权限>
end
io.open = function(filename,mode) -- 重定义io.open
if access_Ok(filename,mode) then
return oldOpen(filename,mode)
else
return nil,"access denied"
end
end
end
经过重定义之后,一个程序就只能通过新的受限版本来调用原来那个未受限的open函数了。上面的示例将原来的不安全版本保存到闭合函数的一个私有变量中,使得外部再也无法直接访问到原来的版本了
6、函数的间接递归
local f,g
function g()
f()
end
function f()
g()
end
上面的代码即在g函数的函数体中调用了f函数,在f函数中调用了g函数。这种情况下就需要使用像第一行那样的前向声明local f,g
七、编译、执行与错误
lua是解释型语言,但是lua允许在运行代码前,先将源代码预编译为一种中间形式。其实区别解释型语言和编译型语言的主要特征不在于是否能编译它们,而是在于编译器是否是语言运行时库的一部分,即是否有能力(并且轻易的)执行动态生成的代码。而Lua是存在了dofile这样的函数,所以可以执行动态生成的代码,因此把lua归为一种解释型的语言。
类似于dofile,loadfile会从一个文件加载lua代码块,但它不会运行代码,只是编译代码,然后将编译结果作为一个函数返回。dofile才会真正执行代码。与dofile不同的是loadfile不会引发错误,它只是返回错误值并不处理错误。所以实际上dofile和loadfile的关系为:
function dofile(filename)
local f = assert(loadfile(filename))
return f()
end
如果loadfile失败,那么assert就会引发错误。
在lua中,函数定义是一种赋值操作。也就是说,它们是在运行时才完成的操作。假设有一个文件foo.lua如下:
function foo(x)
print(x)
end
执行以下代码:
f = loadfile("foo.lua")
在此之后,foo就完成编译了,但是还没有定义它,为了定义它,必须执行以下程序块
print(foo) -- nil
f() --定义foo
foo("ok") -- ok
local path = "/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path,"luaopen_socket")
loadlib函数加载指定的库,并将其链接入lua。不过,它并没有调用库中的任何函数。它将一个C函数作为lua函数返回。如果在加载库或查找初始化函数时发生任何错误,loadlib返回nil及一条错误消息。
使用loadlib是一个非常底层的函数,必须提供库的完整路径及正确的函数名称。
通常使用require来加载C程序库,这个函数会搜素指定的库,然后使用loadlib来加载库。
如果希望在错误发生时,得到更多的调试信息,而不是只有发生错误的位置,那么就需要使用debug库,debug库提供了两个通用的错误处理函数。一个是debug.dubug,它会提供一个Lua提示符,让用户检查错误的原因;另一个是debug.traceback,会根据调用栈来构建一个扩展的错误消息。
print(debug.traceback())
八、协同程序(coroutine)
与线程类似,也是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协同程序共享全局变量和其他部分东西。一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行(即任意时刻只能运行一个协同程序),并且正在运行的协同程序只会在其显示的要求挂起时,它的执行才会暂停。
create用于创建新的协同程序
co = coroutine.create(function() print("hi") end)
print(co) -->thread:0x8735(因为create函数返回的是一个thread类型的值)
协同程序的状态:挂起(suspended)、运行(running)、死亡(dead)、正常(normal)
create后处于挂起状态,可以通过status来检查状态
print(coroutine.status(co))
resume用于启动或再次启动一个协同程序的执行,状态由挂起改为运行,运行后会处于死亡状态
coroutine.resume(co) -->输出hi
函数yield可以让一个运行中的协同程序挂起,而之后再恢复它的运行。
co = coroutine.create(function()
for i = 1,10 do
print("co",i)
coroutine.yield()
end
end
)
此时执行
coroutine.resume(co) -->输出co 1
状态从挂起变为运行,但是运行后并没有死亡,而仍然是处于挂起状态,这代表我们仍可以继续执行运行它
coroutine.resume(co) -->输出co 2
.......................
coroutine.resume(co) -->输出co 10
coroutine.resume(co) -->什么都不打印,运行完这次后协同程序处于死亡状态
当协同程序a唤醒另一个协同程序b时,协同程序a就处于一个特殊的状态,既不是挂起也不是运行,这种状态就叫正常状态(normal)
coroutine.yield()还可以代表协同程序中传入的参数
co = coroutine.create(function()
print("co",coroutine.yield())
end
)
coroutine.resume(co) -->什么都不输出
coroutine.resume(co,4,5) -->输出co 4 5
当一个协同程序结束时,它的主函数所返回的值都将作为对应resume的返回值
co = coroutine.create(function()
return 6,7
end
)
print(coroutine.resume(co)) -->输出true 6 7
coroutine.wrap:比coroutine.create更易于使用,它提供了一个可以唤醒协同程序的函数。但缺点是无法检查wrap所创建的协同程序的状态,也无法检测出运行时的错误
九、数据文件与持久性
1、数据文件
如果要写入的数据为:
Donald E.Knuth,Literate Programming,CSLI,1992
Jon Bentley,More Programming Pearls,Addison-Wesley,1990
现在可以改为:
Entry{
"Donald E.Knuth",
"Literate Programming",
"CSLI",
"1992"
}
Entry{
"Jon Bentley",
"More Programming Pearls",
"Addison-Wesley",
"1990"
}
注意:Entry{<code>}和Entry({<code>})是完全等价的,都是以一个table作为参数来调用函数Entry。
如果要读取文件中的内容,需要首先计算数据文件中条目的数量:
local count = 0
function Entry(_) count = count +1 end
dofile("data")
print("number of entries:" .. count)
下面代码用于读取文件中的内容,并打印出来(读取的是数据中的图书的作者名,即两条Entry的第一个字段)
local authors = {}
function Entry(b) authors[b[1]] = true end
dofile("data")
for name in pairs(authors) do print(name) end
2、串行化
通常需要串行化一些数据,也就是将数据转换为一个字节流或字符流,然后就可以将其存储到一个文件中。
编写创建一个值的代码
function serialize(o)
if type(o) == "number" then
io.write(o)
elseif type(o) == "string" then
io.write(string.format("%q",o))
elseif type(o) == "table" then
io.write("{\n")
for k,v in pairs(o) do
io.write(" [")
serialize(k)
io.write("] = ")
serialize(v)
io.write(",\n")
end
io.write("}\n")
else
error("cannot serialize a" .. type(o))
end
end
尽管这个函数较简单,但是去可以完成保存数字、字符串和树结构table或嵌套的table值的功能
上面代码中保存的table是无环的table
下面介绍保存有环的table
function basicSerialize(o)
if type(o) == "number" then
return tostring(o)
else
return string.format("%q",o)
end
end
function save(name,value,saved)
saved = saved || {} // 初始值
io.write(name,"=")
if type(value) == "number" or type(value) == "string" then
io.write(basicSerialize(value),"\n")
elseif type(value) == "table" then
if saved(value) then // 该value是否已经保存过
io.write(saved[value],"\n") // 使用先前的名字
else
saved[value] = name // 为下次使用保持名字
io.write("{}\n") // 创建一个新的table
for k,v in pairs(value) do // 保存其字段
k = basicSerialize(k)
local fname = string.format("%s[%s]",name,k)
save(fname,v,saved)
end
end
else
error("cannot save a" .. type(value))
end
end
那么什么叫有环的table呢,如下:
a = {x=1,y=2;{3,4,5}}
a[2] = a
a.z = a[1]
调用上面的方法后
save("a",a)将它保存为:
a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]
十、元表和元方法
1、算数类的元方法
像其他语言中的重写
例如默认情况下两个table是不能使用+相加的
但是通过元表可以定义如何计算表达式a+b(其中a和b是两个table)
当将两个table相加时,它会先检查两者之一是否有元表,然后检查该元表中是否有一个叫__add的方法,如果找到了,就调用该元方法
table和userdata可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表,lua在创建新的table时不会创建元表
例如
t = {}
print(getmetatable(t))会输出nil
相应的可以使用setmetatable来设置或修改任何table的元表
t1 = {}
setmetatable(t,t1)
getmetatable(t)会输出t1
任何table都可以作为任何值的元表,此元表描述了它们的行为
一个table甚至可以作为它自己的元表
但是在lua的代码中,只能设置table的元表。若要设置其他类型的值的元表,则必须通过C代码来完成。
print(getmetatable("hi")) 输出table的地址
print(getmetatable(10)) 输出nil
下面的示例用来演示如何计算两个table的并集和交集,为了保持名称空间的整齐,将这些函数存入一个名为Set的table中
local mt = {}
Set = {}
// 根据参数列表中的值创建一个新的集合
function Set.new(1)
local set = {}
setmetatable(set,mt) // 把set的元表设置为mt
for _,v in ipairs(1) do set[v] = true end
return set
end
function Set.union(a,b)
local res = Set.new{}
for k in pairs(a) do res[k] = true end
for k in pairs(b) do res[k] = true end
return res
end
functiong Set.intersection(a,b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
function Set.tostring(set)
local l = {}
for e in pairs(set) do
l[#l + 1] = e
end
return "{" .. table.concat(1,", ") .. "}"
end
function Set.print(s)
print(Set.tostring(s))
end
mt.__add = Set.union // mt是元表,此后只要lua试图将两个集合相加,它就会调用Set.union函数,并将两个操作数作为参数传入,这时候就可以使用加号来求两个table的并集了
mt.__mul = Set.intersection // 类似的,可以使用乘号来求集合的交集
函数定义完毕
使用Set的new方法创建两个table
s1 = Set.new({10,20,30,50})
s2 = Set.new({30,1})
print(getmetatable(s1))
print(getmetatable(s2))
可以看到上面两句的输出结果相同,这说明s1和s2使用的是一个元表
s3 = s1 + s2
Set.print(s3) // 输出{1,10,20,30,50}
Set.print(s3*s1) // 输出{10,20,30,50}
在元表中,每种算数操作符都有对应的字段名。除了上述的__add和__mul外,还有__sub、__div、__unm(相反数)、__mod(取模)和__pow(乘幂)
上面例子中创建的s1和s2都有元表mt,但是如果其中某个操作数没有元表或都没有元表,那么就按照下面的方式处理
lua中查找元表的步骤为:
如果第一个值有元表,并且元表中有__add字段,那么lua就以这个字段为元方法,而与第二个值无关。反之,如果第二个值有元表并含有__add字段,lua就以此字段为元方法,与第一个值无关。如果两个值都没有元方法,lua就会引发一个错误
2、关系类的元方法
元表还可以指定关系操作符的含义,__eq(等于)、__lt(小于)和__le(小于等于),而~=、>、>=则没有单独的元方法,但是可以把~=转化为not(a==b),将a>b转化为b<a,将a>=b转化为b<=a
那么接着上面的例子继续实现
mt.__le = function(a,b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
mt.__lt = function(a,b)
return a <= b and not (b <= a)
end
mt.__eq = function(a,b)
return a <= b and b <= a
end
接下来就可以比较集合了
s1 = Set.new{2,4}
s2 = Set.new{4,10,2}
print(s1<=s2) //输出true,这说明s1是s2的子集
注意:与算数类的元方法不同的是,关系类的元方法不能应用于混合的类型。比如如果试图将一个字符串与一个数字作顺序性比较,lua会引发一个错误。只有当两个比较对象共享一个元方法时,lua才调用这个比较的元方法
其实setmetatable方法和getmetatable方法是修改或访问了元表的一个叫做__metatable的属性,如果想要使用户调用getmetatable方法的时候获取不到它使用的是哪个元表,那么可以对这个属性进行设置,设置后当调用setmetatable方法时就会发生一个错误,示例如下:
mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) // 输出not your business
setmetatable(s1,{}) // 会发生错误,stdin:1:cannot change protected metatable
3、table访问的元方法
有两种可以改变table的行为:查询table及修改table中不存在的字段
(1)__index元方法:
当访问一个table中不存在的字段时,得到的结果为nil,实际上这些访问会促使解释器去查找一个叫__index的元方法。如果没有这个元方法,那么访问结果为nil,否则由这个元方法来提供最终结果
Window = {}
Window.prototype = {x = 0,y = 0,width = 100,height = 100}
Window.mt = {} // 创建元表
function Window.new(o)
setmetatable(o,Window.mt)
return o
end
Window.mt.__index = function(table,key)
return Window.prototype[key]
end
此时,创建一个新窗口,并查询一个它没有的字段
w = Window.new({x = 10,y = 20})
print(w.width) // 输出100
w没有width字段,但是她设置了元表mt,元表mt的__index方法中又width字段,所以就返回了100
(2)修改table中不存在的字段
__newindex元方法
十一、环境
lua将所有全局变量保存在一个table中,这个table称为“环境”,lua又将这个table保存在一个全局变量_G中,所以如果想打印当前环境中所有的全局变量的名称:
for n in pairs(_G) do print(n) end
十二、模块与包
使用了两个函数
require:用于使用模块
module:用于创建模块
(1)require函数的使用
一个模块就是一个程序库,可以通过require来加载,然后就得到了一个全局变量,表示一个table。这个table就像是个名称空间,其内容就是模块中导出的所有东西。一个规范的模块还应使require返回这个table
require "mod"
mod.foo()
如果希望使用较短的模块名称,可以设置一个局部变量
local m = require "mod"
m.foo()
或是给一个局部变量赋值为模块中的某个函数名
require "mod"
local f = mod.foo
f()
以下代码详细说明了require的行为:
function require(name)
if not package.loaded[name] then // 检查模块是否已加载
local loader = findloader(name) // 未加载的话为该模块找一个加载器
if loader == nil then
error("unable to load module" .. name)
end
package.loaded[name] = true // 将模块标记为已加载,为了如果一个模块要求加载另一个模块,而后者又要递归的加载前者,那么后者的require调用就会马上返回,从而避免了无限循环
local res = loader(name) // 初始化模块
if res ~= nil then
package.loaded[name] = res
end
end
return package.loaded[name] // 如果一个模块已经加载,那么后续的require调用都将返回同一个值,不会再次加载
end
搜索一个文件时,lua采用的是一连串的模式,其中每项都是一种将模块名转换为文件名的方式,每项还可以包含一个可选的问号。require会用模块名来替换每个?,然后根据替换的结果来检查是否存在这样一个文件。如果不存在,就尝试下一项,路径中的每项以分号隔开。例如:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
例如调用require "sql",就会试着打开以下文件:
sql
sql.lua
c:\windows\sql
usr/local/lua/sql/sql.lua
(2)module函数的使用
module(...)会创建一个新的table,并将其赋予适当的全局变量和loaded table,最后还会将这个table设为主程序块的环境
默认情况下,module不提供外部访问,必须在调用它之前,为需要访问的外部函数或模块声明适当的局部变量。也可以通过继承来实现外部访问,只需要在调用module时加一个选项package.seeall
module(...,package.seeall)
在一个模块文件的开头有了这句调用后,后续所有的代码就可以像普通的lua代码那样编写了。
十三、面向对象编程
(1)self
lua中的冒号可以用来代替self
a = {balance = 0}
function a.withdraw(self,v)
self.balance = self.balance - v
end
a.withdraw(a,10)
print(a.balance) // 输出-10
为了省略self,上面代码等价于
a = {balance = 0}
function a:withdraw(v)
self.balance = self.balance - v
end
a:withdraw(10)
print(a.balance) // 输出-10
冒号的作用是在一个方法定义中添加一个额外的隐藏参数,以及在一个方法调用中添加一个额外的实参。冒号只是一种语法便利,并没有引入任何新的东西。
例如用点语法定义一个函数,同样可以利用冒号语法来调用它,反之,用一个冒号语法定义一个函数,也可以使用点语法来调用。
a = {balance = 0}
function a.withdraw(self,v)
self.balance = self.balance - v
end
a:withdraw(10)
print(a.balance) // 输出-10
(2)类
lua没有类的概念,每个对象只能自定义行为和形态。但是可以利用原型prototype来实现继承。
如果有两个对象a和b,要让b作为a的一个原型,只需要如下语句:
setmetatable(a,{__index = b})
这样,当访问a的某一个属性,而a不存在该属性时,就会去b上去寻找该属性。(因此我们可以认为b是对象a的“类”)
lua中可以在子类(其实就是个table)重定义那些从基类继承的方法,也可以在子类中定义自己独有的方法。
上面如果a继承b,那么需要设置a的元表的__index为b
依照这个思路,要想让a继承自多个类,那么a的元表的__index应该为一个函数(具体见书中介绍)
(3)私密性
lua没有提供私密性机制,但是可以通过一种方法解决,这种方法就是:
通过两个table来标识一个对象,一个table用来保存对象的状态,另一个用于对象的操作。对象本身是通过第二个table来访问的。
表示状态的table只保存在方法的closure(即闭合函数)中
function newAccount(initialBalance)
local self = {balance = initialBalance}
local withdraw = function(v)
self.balance = self.balance - v
end
local getBalance = function()
return self.balance
end
return{
withdraw = withdraw,
getBalance = getBalance
}
end
调用:
acc1 = newAccount(100)
acc1.withdraw(40)
print(acc1.getBalance()) // 输出60
这种思路不需要额外的参数self,因为那些方法可以直接访问创建的local self,所以无需使用冒号来操作对象,只需要像普通函数那样来调用这些方法即可。
这种思路的私密性在于,通过newAccount创建出table后,就只能通过withdraw方法或getBalance方法来改变或获取self中的值了,而不能直接修改self。
十四、弱引用table
弱引用table是一种机制,用户用它来告诉lua一个引用不应该阻碍一个对象的回收。
即一种会被垃圾收集器忽视的对象引用。如果一个对象的所有引用都是弱引用,那么lua就可以回收这个对象了。如果一个对象只被一个弱引用table所持有,那么最终lua会回收这个对象。
有3种弱引用table:
具有弱引用key的table
具有弱引用value的table
同时具有两种弱引用的table
不论是哪种table,只要有一个key或value被回收了,那么它们所在的整个条目都会从table中删除
一个table的弱引用类型是通过其元表的__mode字段来决定的。如果这个字段的值中包含字母k,那么这个table的key就是若引用的;如果包含字母v,那么这个table的value就是弱引用的。如果值为"kv",那么就属于第三种弱引用table
lua只回收弱引用table中的对象,而像数字和布尔这样的值是不可回收的
十五、IO库
分为简单模型和完整模型
1、简单模型
简单模型的所有操作都作用于两个当前文件。
用io.input和io.output可以改变这两个当前文件。
io.input(filename)调用会以只读模式打开指定的文件,之后除非再次调用io.input否则所有的输入都将来源于这个文件。
io.read从标准输入中读入(可以读入所有,也可以读入一行,也可以读入一个数字,具体见文章),io.write接受任意数量的字符串参数,并写入输出文件。
2、完整模型
等价于C语言中的流,表示一个具有当前位置的打开文件。
要打开一个文件,可以使用io.open函数。io.open(filename,mode)其中mode可以为r可以为w可以为a(追加)可以为b(打开二进制文件)
io库提供了3个预定义C语言流的句柄:io.stdin、io.stdout、io.stderr
使用时是:io.stderr:write(message)
要临时改变当前输入文件,可以这么做:
local temp = io.input() // 保存当前文件
io.input("newinput") // 打开一个新的当前文件
<对新的输入文件做一些操作>
io.input():close() // 关闭当前文件
io.input(temp) // 恢复原来的输入文件
3、其他文件操作
io.flush()会刷新当前输出文件
f:flush()会刷新某个特定的文件f
f:seek可以获取和设置一个文件的当前位置
获取文件大小的方法:
function fsize(file)
local current = file:seek() // 获取当前位置
local size = file:seek("end") // 获取文件大小
file:seek("set",current) // 恢复位置
return size
end
十六、操作系统库
对于文件操作而言,一个是用于文件改名的os.rename函数,一个是用于删除文件的os.remove函数。
日期和时间使用os的time函数和date函数
当前cpu时间的秒数使用os.clock(),一般可以用于计算一段代码所执行的时间:
local x = os.clock()
<执行一段代码>
print(string.format("elapsed time:%.2f\n",os.clock() - x))
中止当前程序的执行用os.exit()方法
获取一个环境变量的值:os.getenv(varname)
os.execute可以用来运行一条系统命令,等价于C语言的system函数
例如创建新目录:
function createDir(dirname)
os.execute("mkdir" .. dirname)
end
十七、调试库
调试库由两类函数组成:自省函数和钩子
自省函数允许检查一个正在运行中程序的各个方面:活动函数栈、当前执行的行、局部变量的名称和值
钩子则允许跟踪一个程序的执行
栈层的概念:一个栈层是一个数字,表示某一时刻某个活动的函数(即一个已被调用但尚未返回的函数),调用调试库的函数是层1,调用这个函数的函数是层2
1、自省函数
主要的自省函数是debug.getinfo函数
当为某个函数foo调用debug.getinfo(foo)时会得到一个table,其中包含一些与该函数相关的信息
当用一个数字调用debug.getinfo(n)时可以得到相应栈层上函数的数据。如果数字n为1,就可以得到调用debug.getinfo的那个函数的数据。如果n大于栈中函数的总数时,debug.getinfo返回nil
访问局部变量可以使用debug.getlocal方法,参数分别为希望查询的函数栈层,一个是变量的索引
访问非局部变量使用函数debug.getupvalue,被一个函数所引用的非局部变量会一直存在着,即使这个引用它的函数已经执行完毕了。
修改非局部变量使用函数debug.setupvalue
所有自省函数都接受一个可选的协同程序参数作为第一个参数,这样就可以从外部来检查这个协同程序。
co = coroutine.create(function()
local x = 10
coroutine.yield()
error("some error")
end)
coroutine.resume(co)
print(debug.traceback(co))
输出结果为:
stack traceback:
[C]:in function 'yield'
temp:3:in function<temp:1>
说明追溯没有进行到resume调用,因为协同程序和主程序运行在不同的栈上
2、钩子
钩子函数被注册后,该函数可以在程序运行中某个特定事件发生时被调用。触发钩子的事件可以有四种:
(1)每当lua调用一个函数产生call事件
(2)每当函数返回时产生return事件
(3)每当lua开始执行一行新代码时产生line事件
(4)当执行完指定数量的指令后产生count事件
lua用一个字符串参数来调用钩子函数:"call"、"return"、"line"、"count"
注册一个钩子,需要用两个或三个参数来调用debug.sethook,第一个参数是钩子函数,第二个参数是一个字符串("call"、"return"、"line"、"count"中的一个),第三个参数是一个可选的数字,用于说明多久获得一次count事件
3、性能剖析
如果是做计时性的剖析,最好使用C接口,因为每次lua调用钩子的代价太高了
十八、C API概述
C API是一组能使C代码与Lua交互的函数。其中包括读写Lua全局变量、调用Lua函数、运行一段Lua代码,以及注册C函数以供Lua代码调用
Lua的解释器程序lua.c就是应用程序代码的一个实例,而lua标准库则是库代码的实例
lua与C通信的主要方法是一个无所不在的虚拟栈(lua严格按照后进先出的规范来操作这个栈),所有API调用都会操作这个栈上的值。栈可以解决lua使用垃圾收集器而C语言要求显式地释放内存的差异,也可以解决lua使用动态类型而C语言使用静态类型的差异
lua可以同时作为C代码或C++代码来编译。
C API中的错误处理:C语言不同于C++和Java,它没有提供异常处理机制。
十九、编写C函数的技术
1、对于一个lua函数来说,有3种地方可以存放非局部的数据,它们是全局变量、函数环境、非局部的变量(closure中)
2、C API也提供了3种地方来保存这类数据:注册表、环境和upvalue
注册表是一个全局的table,只能被C代码访问(用于保存需要在几个模块中共享的数据)
环境可以保存一个模块的私有数据,一个模块内的所有函数共享一个环境table
upvalue是一种与特定函数相关联的lua值,实现了一种类似于C语言中静态变量的机制,这种变量只在一个特定的函数中可见
(注册表、环境、upvalue的概念具体见书的27章)
二十、用户自定义类型
使用C语言编写新的类型来扩展Lua(可以节省内存)
userdata提供了一块原始的内存区域,可以用来存储任何东西。如果由于某些原因,需要通过其他机制来分配内存,那么可以创建只有一个指针大小的userdata,
然后将指向真正内存块的指针存入其中。
二十一、线程和状态
1、线程
lua不支持真正的多线程,也就是不支持那种共享内存的抢先式多线程。
lua的线程(也就是协同程序)是协作式的,因此可以避免由不可预知的线程切换所带来的问题。
另外,lua的多个状态之间是不共享内存的。
一个线程就是一个栈,每个栈都保留着一个线程中所有未完成的函数调用信息,这些信息包括调用的函数、每个调用的参数和局部变量。
多个线程就意味着多个独立的栈。
只要创建一个lua状态,lua就会自动在这个状态中创建一个新线程,这个线程称为"主线程"。
主线程永远不会被回收,当使用lua_close关闭状态时,它会随状态一起释放。而其他线程却是垃圾回收的对象。
调用lua_newthread便可以在一个状态中创建其他的线程,当在一个线程中创建一个新的线程时,新线程就以一个空栈开始运行,老线程的栈顶就是这个新线程,新线程压入老线程的栈顶,这样就能确保新线程不会成为垃圾。
在一个线程中调用一个函数,就会将函数压入栈中,并压入其参数,最后在调用方法时传入参数的数量。如果正在运行的函数交出(yield)了控制权,lua_resume就会返回一个特殊的代码LUA_YIELD,并将线程置于一个可以被再次恢复执行的状态。
2、状态
每次调用lua_newstate都会创建一个新的lua状态,不同的状态是各自独立的,它们之间并不共享数据。
3、通道(channel)
通道只是一些用于匹配发送者和接收者的简单字符串。一个线程在将消息发送到指定通道时,会一直处于阻塞状态,直到另外一个进程对这个通道进行了接收操作。
当一个线程在接收一个通道时,也会处于阻塞状态,直到另一个进程向这个通道进行了发送操作。
4、进程
一个新的Lua进程需要一个新的C线程,而一个C线程需要一个函数体。
为了创建并运行一个新进程,系统必须创建一个新的lua状态、启动一个新线程、编译指定的程序块、调用程序块,最后释放这些资源(原线程会做前3件事,而新线程会做其余的事)。
更高效创建进程的方式:
创建进程需要首先创建一个新的lua状态,然后打开所有的标准库则需要花费十倍于创建新状态的时间。
大部分进程可能并不需要用到所有的标准库,而只需用到一到两个库。
可以对库进行预注册来避免打开一个无用库的代价,相对于为每个标准库调用luaopen_*函数,使用这种方法时只需将每个标准库的打开函数放入package.preload中即可。
如果进程调用了require "lib",那么require就会调用与这个lib关联的函数,从而打开这个库。
一般情况下都需要打开base库,另外还需要package库。如果没有package库,就无法通过require来打开其他库。
其他所有的库都是可选的。当打开一个新状态时,使用openlibs函数来代替luaL_openlibs
如果一个进程需要其中任意一个库,只需显式的require它,require就会调用相应的luaopen_*函数了。