Lua 数据类型
Lua是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua中有8个基本类型分别为:nil、boolean、number、string、userdata、function、thread和table。
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean 包含两个值:false和true。Lua 把 false 和 nil 看作是"假",其他的都为"真":
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字或者是字符串。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。
全局变量
在默认情况下,变量总是认为是全局的。
全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。
例如:
b=1;
print(b);
输出结果:1
print(a);
输出结果:nil
如果你想删除一个全局变量,只需要将变量赋值为nil。
string(字符串)
字符串或串(String)是由数字、字母、下划线组成的一串字符。
Lua 语言中字符串可以使用以下三种方式来表示:
1、单引号间的一串字符。
2、双引号间的一串字符。
3、[[和]]间的一串字符。
以上三种方式的字符串实例如下:
string1 = "hello"
print("\"字符串 1 是\"",string1)
string2 = 'china'
print("字符串 2 是",string2)
string3 = [["hello china"]]
print("字符串 3 是",string3)
运行结果:
"字符串 1 是" hello
字符串 2 是 china
字符串 3 是 "hello china"
字符串操作
Lua 提供了很多的方法来支持字符串的操作:
1、string.upper(argument) 字符串全部转为大写字母。
2、string.lower(argument) 字符串全部转为小写字母。
3、string.gsub(mainString,findString,replaceString,num) 在字符串中替换,mainString为要替换的字符串, findString 为被替换的字符,replaceString 要替换的字符,num 替换次数(可以忽略,则全部替换)
4、string.find (str, substr, [init, [end]]) 在一个指定的目标字符串中搜索指定的内容(第三个参数为索引),返回其具体位置。不存在则返回 nil。
5、string.format(...) 返回一个类似printf的格式化字符串
6、string.char(arg) 和 string.byte(arg[,int]) char 将整型数字转成字符并连接, byte 转换字符为整数值(可以指定某个字符,默认第一个字符)。
7、.. 链接两个字符串
8、string.gmatch(str, pattern) 回一个迭代器函数,每一次调用这个函数,返回一个在字符串 str 找到的下一个符合 pattern 描述的子串。如果参数 pattern 描述的字符串没有找到,迭代函数返回nil。
字符串拼接,用“ ..”
9、string.match(str, pattern, init) string.match()只寻找源字串str中的第一个配对. 参数init可选, 指定搜寻过程的起点, 默认为1。 在成功配对时, 函数将返回配对表达式中的所有捕获结果; 如果没有设置捕获标记, 则返回整个配对字符串. 当没有成功的配对时, 返回nil。
匹配模式
Lua 中的匹配模式直接用常规的字符串来描述。 它用于模式匹配函数 string.find, string.gmatch, string.gsub, string.match。
你还可以在模式串中使用字符类。
字符类指可以匹配一个特定字符集合内任何字符的模式项。比如,字符类%d匹配任意数字。所以你可以使用模式串 '%d%d/%d%d/%d%d%d%d' 搜索 dd/mm/yyyy 格式的日期:
s = "today is 20/09/2017, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date))) --> 20/09/2017
下面的表列出了Lua支持的所有字符类:
单个字符(除 ^$()%.[]*+-? 外): 与该字符自身配对
1).(点): 与任何字符配对
2)%a: 与任何字母配对
3)%c: 与任何控制符配对(例如\n)
4)%d: 与任何数字配对
5)%l: 与任何小写字母配对
6)%p: 与任何标点(punctuation)配对
7)%s: 与空白字符配对
8)%u: 与任何大写字母配对
10)%w: 与任何字母/数字配对
11)%x: 与任何十六进制数配对
12)%z: 与任何代表0的字符配对
13)%x(此处x是非字母非数字字符): 与字符x配对. 主要用来处理表达式中有功能的字符(^$()%.[]*+-?)的配对问题, 例如%%与%配对
14)[数个字符类]: 与任何[]中包含的字符类配对. 例如[%w_]与任何字母/数字, 或下划线符号(_)配对
15)[^数个字符类]: 与任何不包含在[]中的字符类配对. 例如[^%s]与任何非空白字符配对
当上述的字符类用大写书写时, 表示与非此字符类的任何字符配对. 例如, %S表示与任何非空白字符配对.例如,'%A'非字母的字符:
print(string.gsub("hello, up-down!", "%A", "."));
运行结果:
hello..up.down. 4
数字4不是字符串结果的一部分,他是gsub返回的第二个结果,代表发生替换的次数。
在模式匹配中有一些特殊字符,他们有特殊的意义,Lua中的特殊字符如下:
( ) . % + - * ? [ ^ $
'%' 用作特殊字符的转义字符,因此 '%.' 匹配点;'%%' 匹配字符 '%'。转义字符 '%'不仅可以用来转义特殊字符,还可以用于所有的非字母的字符。
模式条目可以是:
1)单个字符类匹配该类别中任意单个字符;
2)单个字符类跟一个 '*', 将匹配零或多个该类的字符。 这个条目总是匹配尽可能长的串;
3)单个字符类跟一个 '+', 将匹配一或更多个该类的字符。 这个条目总是匹配尽可能长的串;
4)单个字符类跟一个 '-', 将匹配零或更多个该类的字符。 和 '*' 不同, 这个条目总是匹配尽可能短的串;
5)单个字符类跟一个 '?', 将匹配零或一个该类的字符。 只要有可能,它会匹配一个;
下面是一个例子,打印出字符串中所有单词,代码如下:
s = " hello, word!"
for v in string.gmatch(s, "%a+") do
print(v);
end
运行结果:
hello
word
例如:
Lua 运算符
运算符是一个特殊的符号,用于告诉解释器执行特定的数学或逻辑运算。Lua提供了以下几种运算符类型:
1)算术运算符
2)关系运算符
3)逻辑运算符
4)其他运算符
算术运算符
+ - * / % ^ -
关系运算符
== ~= > < >= <=
逻辑运算符
and or not
逻辑操作符有and 、or 和 not。与条件控制语句一样,所有的逻辑操作符将false和nil操作符视为假,其它值视为真。下面直接看代码:
print(4 and 5); -- 5
print(nil and 13); -- nil
print(false and 13); -- false
print(4 or 5); -- 4
print(false or 5); -- 5
解析:
and:第一个操作符为假,返回第一个,否则,返回第二个
or:第一个为真,返回第一个,否则第二个
常用的Lua 习惯写法:
x = x or v 等价于
if not x then x = v end
(a and b) or c 等价于
a ? b:c
例如:为了选出数字x和y中的较大者,可以使用以下语句:
max = (x > y) and x or y
分析:若 x > y,则and第一个操作数为真,那么and操作数的结果是第二个操作数,当 x > y 为假,and表达式为假,因此 or 的结果是第二个操作数。
其他运算符
.. 连接两个字符串
# 一元运算符,返回字符串或表的长度。
运算符优先级
从高到底的顺序:
^ not - (unary) * / + - .. < > <= >= ~= == and or
其中,^和..二元运算符是从右连接,其它二元运算符都是左连接的。
Lua函数
函数定义
Lua 编程语言函数定义格式如下:
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn) function_body return result_params_comma_separated end
解析:
1、optional_function_scope
: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。
2、function_name:
指定函数名称。
3、argument1, argument2, argument3..., argumentn:
函数参数,多个参数以逗号隔开,函数也可以不带参数。
4、function_body:
函数体,函数中需要执行的代码语句块。
5、result_params_comma_separated:
函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。
实例:
function max(a)
key = 1;
m = a[key];
for i, v in pairs(a) do
if(v>m) then
m = v;
key = i;
end
end
return key,m;
end
arr ={23,4,45,2,5,2,35,64,3}
i,value = max(arr);
print("index="..i.." value="..value);
注意:Lua函数可以返回多值。上例中就分别返回最大值下标与最大值,返回多个值之间用逗号隔开。
可变参数
Lua函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(...) 表示函数有可变的参数。
Lua将函数的参数放在一个叫arg的表中,#arg 表示传入参数的个数。
function avarege(...)
local arg = {...};
local result = 0;
for i, v in ipairs(arg) do
result = result + v;
end
return result / #arg
end
value = avarege(1,3,4);
print("value="..value);
Lua 迭代器与泛型for
7.1迭代器与closure
所谓“迭代器”就是一种可以遍历一种集合中所有元素的机制。在Lua中,通常将迭代器表示为函数。每调用一次函数,即返回集合中的“下一个”元素。每次迭代器都需要在成功调用后保持一些状态,这样才能知道它当前所在的位置及如何执行下一个位置。closure对这类任务提供了极佳的支持,一个closure就是一种可以访问其外部嵌套分举办变量函数。
一个closure结构涉及两个函数:closure本身和一个用于创建closure的工厂函数。
下面是创建该该类迭代器,如下代码:
function values(t)
local i = 0 ;
return function()
i = i + 1;
return t[i];
end
end
在本例中,values就是一个工厂,每当调用这个工厂时,就会创建一个新的closure(集迭代器本身)。这个closure将它的状态保存在其外部变量t和i中。每当调用这个迭代器时,它就从列表t中返回下一个值,直到最后一个元素返回,迭代器就会返回nil,以表示迭代结束。
在while中使用该迭代器,代码如下:
t = {1,3,5,7}
local iter = values(t); --创建迭代器
while true do
local element = iter(); --调用迭代器
if(element == nil) then
return
end
print(element);
end
在for中使用迭代器,代码如下:
-- for 循环进行迭代
for v in values(t) do
print(v);
end
7.2 泛型for的语义
泛型for循环过程中内部保存了迭代器函数。实际它保存了三个值:迭代器函数、恒定状态、控制变量。上例中 iter就是跌代器,t就是恒定状态
7.3 无状态的迭代器
无状态迭代器,就一种自身不保存任何状态的迭代器,因此,我们可以在多个循环中使用同一个无状态的迭代器,避免创建新的closure开销。下面是一个无状态的迭代器例子,代码如下:
--无状态迭代器1
function iter(t,i)
i = i+1;
local v = t[i];
if v then
return i,v;
end
end
function ipair(t) --创建无状态迭代器,没有保存任何状态
return iter,t ,0
end
t = {1,3,5,7,9,11};
for i,v in ipair(t) do --每次在for中调用迭代器
print("i="..i.." v="..v);
end
当Lua调用for循环中的ipair(t)时,它会获得3个值:迭代器函数iter、恒定状态t,和控制变量的初始值0.然后调用iter(a,0)得到1,a[1],在第二次迭代,继续调用iter(a,1),得到2,a[2],直到得到第一个nil元素。
总结:尽可能地尝试编写无状态的迭代器那些迭代器将所有状态保存在for变量中,不需要在开始一个循坏时创建任何新的对象。其次是使用closure创建迭代器,因为创建一个closure比创建一个table更廉价,访问比table更快。
local tab= {
[1] = "a", //等同于下标为1
[4] = "b",
[6] = "c"
}
for i,v in pairs(tab) do -- 输出 "a" ,"b", "c" ,
print( tab[i] )
end
for i,v in ipairs(tab) do -- 输出 "a" ,k=2时返回nil,断开
print( tab[i] )
end
八、Lua 错误处理
程序运行中错误处理是必要的,在我们进行文件操作,数据转移及web service 调用过程中都会出现不可预期的错误。如果不注重错误信息的处理,就会照成信息泄露,程序无法运行等情况。
任何程序语言中,都需要错误处理。错误类型有:
1、语法错误
2、运行错误
语法错误
语法错误通常是由于对程序的组件(如运算符、表达式)使用不当引起的。如下代码:
a == 2
执行以上程序会出现如下错误:
lua: test2.lua:2: 'do' expected near 'print'
语法错误比程序运行错误更简单,运行错误无法定位具体错误,而语法错误我们可以很快的解决,如以上实例我们只要在for语句下添加 do 即可:
for a= 1,10 do print(a) end
8.1 运行错误
运行错误是程序可以正常执行,但是会输出报错信息。如下实例由于参数输入错误,程序执行时报错:
function add(a,b) return a+b end add(10)
当我们编译运行以下代码时,编译是可以成功的,但在运行的时候会产生如下错误:
lua: test2.lua:2: attempt to perform arithmetic on local 'b' (a nil value) stack traceback: test2.lua:2: in function 'add' test2.lua:5: in main chunk [C]: ?
以下报错信息是由于程序缺少 b 参数引起的。
8.2 错误处理
我们可以使用两个函数:assert 和 error 来处理错误。实例如下:
local function add(a,b) assert(type(a) == "number", "a 不是一个数字") assert(type(b) == "number", "b 不是一个数字") return a+b end add(10)
执行以上程序会出现如下错误:
lua: test.lua:3: b 不是一个数字 stack traceback: [C]: in function 'assert' test.lua:3: in local 'add' test.lua:6: in main chunk [C]: in ?
实例中assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。
8.3 error函数
语法格式:
error (message [, level])
功能:终止正在执行的函数,并返回message的内容作为错误信息(error函数永远都不会返回)
通常情况下,error会附加一些错误位置的信息到message头部。
Level参数指示获得错误的位置:
1、Level=1[默认]:为调用error位置(文件+行号)
2、Level=2:指出哪个调用error的函数的函数
3、Level=0:不添加错误位置信息
8.4 pcall 和 xpcall、debug
pcall以一种"保护模式"来调用第一个参数,因此pcall可以捕获函数执行中的任何错误。
通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但pcall返回时,它已经销毁了调用桟的部分内容。
Lua提供了xpcall函数,xpcall接收第二个参数——一个错误处理函数,当错误发生时,Lua会在调用桟展看(unwind)前调用错误处理函数,于是就可以在这个函数中使用debug库来获取关于错误的额外信息了。
debug库提供了两个通用的错误处理函数:
1、debug.debug:提供一个Lua提示符,让用户来价差错误的原因
2、debug.traceback:根据调用桟来构建一个扩展的错误消息
xpcall 使用实例 ,代码如下:
function testfunction()
n = n/nil;
end
function errorhandler(err)
print("ERROR:",err);
end
status = xpcall(testfunction, errorhandler);
print(status);
执行以上程序会出现如下错误:
ERROR: demon/error/demon_error.lua:2: attempt to perform arithmetic on global 'n' (a nil value)
false
Lua 数组
数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。
Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。
一维数组
array = {"eweqf","wefre"}
for i = 1,#array do
v = array[i]
print(v);
end
多维数组
多维数组即数组中包含数组或一维数组的索引键对应一个数组。
以下是一个三行三列的阵列多维数组:
-- 初始化数组
array = {}
for i=1,3 do
array[i] = {}
for j=1,3 do
array[i][j] = i*j
end
end
-- 访问数组
for i=1,3 do
for j=1,3 do
print(array[i][j])
end
end
Lua 协同程序(coroutine)
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
协同是非常强大的功能,但是用起来也很复杂。
线程和协同程序区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
9.1 协同程序基础
Lua将所有关于协同的函数放置在名为"coroutine"的table中。函数create用于创建新的协同程序,它只有一个参数,即就是一个函数,该函数代码就是协同程序所要执行的内容。create会返回一个thread类型的值,用于表示新的协同程序。
一个协同程序有4种不同的状态:挂起、运行、死亡和正常‘当创建一个协同时,它处于挂起状态。协同程序不会再创建时自动执行其内容,只有在调用resume后执行。
下面是一个简单的协同案例,代码如下:
co = coroutine.create(function() --创建协同
for i = 1, 5 do
print("co",i);
coroutine.yield();
end
end);
coroutine.resume(co); --唤醒协同
9.2 生产者-消费者
现在我就使用Lua的协同程序来完成生产者-消费者这一经典问题。
producer = coroutine.create(function()
while true do
local x = io.read(); --产生新值
send(x);
end
end);
function send(x)
coroutine.yield(x); --传入的值作为resume返回结果
end
function receive()
local status ,value = coroutine.resume(producer); --消费新值
return value;
end
file = io.open("C:/Users/xielianwu/Desktop/test.txt", "r");
io.input(file);
v = receive();
i = 0
while v do
i = i+1;
print("i=",i,v);
v= receive();
end
9.3 协同程序实现迭代器
下面是一个以协同程序来实现迭代器的例子。需求是遍历,某个数组的所有排列组合,并打印结果。代码如下:
function permutations(a) --创建迭代器工厂
local co = coroutine.create(function() --保持迭代器局部变量状态
permgen(a);
end);
return function () --返回迭代器
local code,res = coroutine.resume(co)
return res;
end
end
function permgen(a,n) --排列
local n = n or #a;
if n <= 1 then
coroutine.yield(a);
else
for i = 1, n do
a[n],a[i] = a[i],a[n];
permgen(a,n-1);
a[n],a[i] = a[i],a[n];
end
end
end
function printResult(v) --打印结果
for i = 1, #v do
io.write(v[i]," ");
end
io.write('\n');
end
a = {'a','b','c'};
for v in permutations(a) do
printResult(v);
end
Lua table(表)
table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数字、字典等。
Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。
Lua table 是不固定大小的,你可以根据自己需要进行扩容。
Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用"format"来索引table string。
table(表)的构造
构造器是创建和初始化表的表达式。表是Lua特有的功能强大的东西。最简单的构造函数是{},用来创建一个空表。可以直接初始化数组:
-- 初始化表 mytable = {} -- 指定值 mytable[1]= "Lua" -- 移除引用 mytable = nil -- lua 垃圾回收会释放内存
当我们为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil ,则 b 同样能访问 table 的元素。如果没有指定的变量指向a,Lua的垃圾回收机制会清理相对应的内存。
Lua 模块与包
模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:
--- 文件名为module.lua
--- 定义一个名为module的模块
Module = {};
--定义一个常量
Module.constant="这是一个常量";
--定义一个公共方法
function Module.say()
print("这是一个公有方法");
end
--定义一个私有方法
local function hello()
print("这是一个私有方法");
end
function Module.printf()
hello();
end
return Module;
require 函数
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
require("<模块名>")
或者
require "<模块名>"
执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。
--test1.lua文件
-- module模块为上文提到的module.lua
require("/demon/module")
Module.say();
print(Module.constant)
Module.printf();
执行结果:
这是一个公有方法
这是一个常量
这是一个私有方法
requeire的行为详细说明:
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; --将模块标记为已加载
local res = loader(name) -- 初始化模块
if res ~= nil then
package.loaded[name] = res;
end
end
return package.loaded[name]
end
首先,它在 table package.loaded 中检查模块是否已加载。如果加载了的话,就直接返回相应的值。因此只要一个模块已经加载过,后续的require调用都将返回同一个值,不会再次加载它 ...
避免修改每个函数中的模块名:
local M = {};
Module = M; --模块名
--定义一个常量
M.constant="这是一个常量";
--定义一个公共方法
function M.say()
print("这是一个公有方法");
end
--定义一个私有方法
local function hello()
print("这是一个私有方法");
end
function M.printf()
hello();
end
return M;
模块名参数
在编写模块时,我们可以避免写模块名,因为require会将模块名作为参数传给模块,改进代码如下:
local modename = "Module";
local M = {};
_G[modename] = M;
package.loaded[modename] = M;
--定义一个常量
M.constant="这是一个常量";
--定义一个公共方法
function M.say()
print("这是一个公有方法");
end
--定义一个私有方法
local function hello()
print("这是一个私有方法");
end
function M.printf()
hello();
end
这里有点改进是消除return语句,就是模块table直接赋予给package.loaded,这样就不需要在模块结尾返回M。因为一个模块无返回值时,require就回返回package.loaded[modename]当前值。
使用环境
上面创建一个基本模块时,还是投入一些额外的关注,比如访问同一模块的其它公共实体时,必须限定其名称。并且只要一个函数状态从私有改为公共时,就必须修改调用,另外私有声明容易忘记local。那么这里就要使用"函数环境",让模块的主程序独占一个环境,这样他所有函数和全局变量都记录在这个table中,以下代码:
local modename = "Module";
local M = {};
_G[modename] = M;
package.loaded[modename] = M;
setmetatable(M, {__index = _G})
setfenv(1, M)
--定义一个常量
constant="这是一个常量";
--定义一个公共方法
function say()
print("这是一个公有方法");
end
--定义一个私有方法
local function hello()
print("这是一个私有方法");
end
function printf()
hello();
end
这样在模块中定义和调用该模块中的其它函数时,就不再使用前缀了。
缺点:当创建一个空 table M 作为环境后,就 无法访问前一个环境中的局部变量。比如访问print函数,因为全局环境变成M了,所以为了能够访问原全局环境变量,我们加入该代码:
setmetatable(M, {__index = _G})
module函数
前面的几个实例中,都是以相同模式开始的,代码如下:
local modename = "Module";
local M = {};
_G[modename] = M;
package.loaded[modename] = M;
setmetatable(M, {__index = _G})
setfenv(1, M)
Lua 5.1之后,我们直接可以用以下代码来取代前面的设置代码:
module("Module",package.seeall);
--定义一个常量
constant="这是一个常量";
--定义一个公共方法
function say()
print("这是一个公有方法");
end
--定义一个私有方法
local function hello()
print("这是一个私有方法");
end
function printf()
hello();
end
子模块与包
Lua 支持具有层级性的模块,可以使用一个点来分隔名称中的层级。假如一个模块名为mod.sub,那么它就是mod的一个子模块。以下这段代码:
require("/demon/module")
可以换成
require("demon.module")
12章 数据文件与持久性
数据文件
串行化
Lua 元表(Metatable)
在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。
因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。
当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"__add"的字段,若找到,则调用对应的值。"__add"等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。
有两个很重要的函数来处理元表:
- setmetatable(table,metatable): 对指定table设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败 。
- getmetatable(table): 返回对象的元表(metatable)。
创建或修改元表
可以使用setmetatable来设置或者修改任何table的元素,代码如下:
t1 = {};
t = {}; --元表
setmetatable(t1, t)
一、算术类的元方法
假设用table来表示集合,并且有一些函数用来计算集合的并集和交集等。为了保持名称空间的整齐,则将这些函数存入一个名为Set的table中。代码如下:
Set ={}
local mt = {} -- 集合的元表
function Set.new(l)
local set = {};
setmetatable(set, mt) -- 每个用new方法创建的表都共享同一个元表
for _, v in ipairs(l) do
set[v] = true;
end
return set;
end
-- 并集
function Set.union(a,b)
if getmetatable(a) ~= getmetatable(b) ~= mt then
error("attempt to 'add' a set with a non-set value",2);
end
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
--交集
function Set.intersection(a,b)
if getmetatable(a) ~= getmetatable(b) ~= mt then
error("attempt to 'mul' a set with a non-set value",2);
end
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(l, ", ") .. "}" ;
end
function Set.print(s)
print(Set.tostring(s));
end
s1 = Set.new({10,20,30,50});
s2 = Set.new({30,1});
mt.__add = Set.union; --将元方法加入元表,用于完成如何完成加法的__add字段
s3 = s1 + s2; --只要两个集合相加,就会调用Set.union函数,并将两个操作数作为参数传入,用加号表示集合并集
Set.print(s3);
mt.__mul = Set.intersection; --用乘号表示集合交集
s = (s1+s2)*s1;
Set.print(s)
二、关系类元
元表还可以指定关系操作符的含义,元方法为__eq(等于)、__lt(小于)和__le(小于等于),二其它3个关系操作符,没有单独的元方法,但Lua会如下转换:
1、 a~=b 转换 not(a==b)
2、 a>b 转换 b<a
3、 a>=b 转换 b<=a
下面上面的mt元表提供__le(小于等于)、lt(小于)和eq(等于)元方法,如下代码:
mt.__le元方法:
mt.__le = function(a,b) --小于等于
for i in pairs(a) do
if not b[i] then
return false;
end
end
return true;
end
mt._-lt元方法:
mt.__lt = function(a,b) --小于
return a <= b and not (b <= a);
end
mt.__eq元方法:
mt.__eq = function(a,b) --等于
return a <= b and b <= a;
end
测试:
a1 = Set.new{2,3}
a2 = Set.new{3};
print(a1<=a2);
print(a1<a2);
print(a1>=a2);
print(a1>a2);
print(a2 == a1*a2);
运行结果:
false
false
true
true
true
三、定义库的元方法
各种程序库在元表中定义它们自己的字段是很普遍的方法。元方法都只针对于Lua的核心,也就是一个虚拟机。它会检测一个操作中的值是否存在元表,这些元表中是否定义了关于此操作的元方法。从另一方面说,由于元表也是一种常规的table,所以任何人、任何函数都可以使用它们。
函数tostring就是一个库的元方法。设置元表中的__tostring字段:
mt__tostring = Set.tostring;
此后只要调用print来打印集合,print就会调用tostring函数,进而调用到Set.tostring
保护元表
函数setmetatable和getmetatable也会用到元表中的一个__metatable字段,用于保护元表。当设置了该字段时,getmetatble就会返回该字段的值,setmetatable则会引发一个错误:
mt.__metatable = "not your business";
print(getmetatable(s1)); --not your business
setmetatable(s2, {}) --异常:cannot change a protected metatable
四、table 访问的元方法
算术类和关系类运算符的元方法都为各种错误情况定义了行为,它们不会改变语言的常规行为,但Lua提供了可以改变table行为的方法。有两种可以改变的table行为:查询table及修改中不存在的字段。
4.1 __index元方法
假设创建一些描述窗口的table,每个窗口都有默认的值,位置,大小,颜色等
方法一:让新窗口从原型窗口处继承所有不存在的字段。首先声明一个原型和一个构造函数,构造函数创建一个新的窗口,并共享同一个元表代码如下:
Window = {}; --创建一个名字空间
Window.prototype = {x=0,y=0,width=100,height=100}; --使用默认值来创建原型
Window.mt = {}; --创建元表
Window.new = function(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
分析:若Lua检查到w没有某字段,但是在其元表有一个__index字段,那么Lua就会创建w(table)和"width"(不存在的key)来调用__index元方法,怎么用这个key来索引原型table,并返回结果。
4.2 __newindex元方法
__newindex元方法与__index类似,不同之处是__newindex用于table的更新,__index是用于table的查询。当一个table中不存在的索引赋值时,解析器就会查找__newindex元方法,解析器就调用它,而不是执行赋值;如果这个元方法是一个table,解析器就在此table中执行赋值,而不是对原来的table赋值。组合使用__index和__newinex元方法就可以实现Lua一些强大的功能,比如实现只读table,后面会讲到。
4.2 具有默认值得table
4.3 跟踪table的访问
4.4 只读的table
4.2 设置默认值的table
常规table中的任何字段默认值都是nil,通过元表就可以很容易地修改这个默认值。代码如下:
function setDefaultValue(t,v)
local mt = {__index = function()
return v;
end}
setmetatable(t, mt);
end
a = {x=3,y=2};
print(a.x,a.y,a.z);
setDefaultValue(a, 0)
print(a.x,a.y,a.z);
输出结果: 3 2 nil
3 2 0
在调用setDefault后,任何对tab中不存在的访问都将调用它的_index元方法,这个元方法会返回0(这个元方法v的值)
缺点:如果要创建大量的具有默认值的table时,每个table都会创建一个元表,这样开销比较大。下面是创建大量的具有默认值的table使用一个共同的元表,而默认值存放在每个table本身字段里,代码如下:
------------------------------共用一个元表
local mt2= {__index = function(t)
return t.___;
end}
function setDefaultValue2(t, v)
print(t);
print(t);
t.___ = v;
setmetatable(t, mt2);
end
b = {x=4,y=5}
print(b);
print(b.x,b.y);
setDefaultValue2(b, 1);
print(b.x,b.y,b.d);
这里当b访问一个不存在的字段时,就会找打元表中国__index字段,__index字段值是一个函数,函数参数就是table b。当table访问不存在的字段时,例如:b.z,那么它会将table b作为参数传入__index字段的函数中,返回b.___字段值作为table b的默认值。
跟踪tale的访问
只读的table
通过代理的概念,可以很容易地实现出只读的table。只需要跟踪所有对table的更新操作,并引发一个错误就可以了。所以只读table的实现查询时只需原table代替__index方法,更新table时只需在__newindex方法里抛出错误信息。代码如下:
function readOnly(t)
local proxy = {};
local mt = {
__index = t;
__newindex = function(t,k,v)
error("attemp to update a only read table", 2);
end
};
setmetatable(proxy, mt)
return proxy;
end
days = readOnly{"oneday","twoday","threeday","fourday","fiveday","sixday","sevenday"};
days[2] = "hello china";
当修改days[2]字段的值时,会抛出错误信息:attemp to update a only read table
七、Lua 文件 I/O
Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式。
1、简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
2、完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法
简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。
打开文件操作语句如下:
file = io.open (filename [, mode])
r 以只读方式打开文件,该文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
r+ 以可读写方式打开文件,该文件必须存在。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a+ 与a类似,但此文件可读可写
b 二进制模式,如果文件是二进制文件,可以加上b
+ 号表示对文件既可以读也可以写
7.1 简单模式
简单模式使用标准的 I/O 或使用一个当前输入文件和一个当前输出文件。
以下为 file.lua 文件代码,操作的文件为test.lua(如果没有你需要创建该文件),代码如下:
-- 以读方式打开文件
file = io.open("test.txt", "r");
-- 设置默认输入文件为test.txt
io.input(file);
--输出文件第一行
print(io.read());
--关闭文件
io.close(file);
-- 以附加的方式打开只写文件
file2 = io.open("test.txt", "a");
-- 设置默认输出文件为 test.txt
io.output(file2);
-- 在文件最后一行添加
io.write("我是中国人");
-- 关闭打开的文件
io.close(file2);
7.2 完全模式
-- 以只读方式打开文件
file = io.open("test2.txt", "r")
-- 输出文件第一行
print(file:read())
-- 关闭打开的文件
file:close()
-- 以附加的方式打开只写文件
file = io.open("test2.txt", "a")
-- 在文件最后一行添加 Lua 注释
file:write("--test")
-- 关闭打开的文件
file:close()
九、Lua 面向对象
Lua 中的table就是一种对象。首先table与对象一样可以拥有状态。其次,table也与对象一样拥有独立标示(self),最后与对象一样具有独立的创建者和创建地的生命周期。
类
下面来创建一个简单的类,代码如下:
--创建Account类
Account = {balance = 1000};
function Account:withdraw(v)
self.balance = self.balance - v;
end
function Account:deposit(v)
self.balance = self.balance + v;
end
function Account:new()
local o = {};
setmetatable(o, self);
self.__index = self;
return o;
end
--使用Account类来创建a对象
a = Account:new();
a:withdraw(200);
print(a.balance);
a:deposit(300)
print(a.balance);
解析:
self 参数是相对于java的this指针,这个参数是对程序员隐藏的,只要定义方法是使用冒号定义,就能隐藏该参数,调用是也要用对象加冒号。
冒号的作用是在一个方法定义中添加一个额外的隐藏参数,以及一个方法调用中添加一个额外的实参。冒号只是便利,并没有引入新东西。
继承
由于类也是对象,它们也可以从其他类获得方法。这种行为就是继承,可以很容易在lua中实现,
假设基类为Account:
--基类
Account = {balance = 1000};
function Account:withdraw(v)
if v > self.balance then
error("insufficient funds")
end
self.balance = self.balance - v;
end
function Account:deposit(v)
self.balance = self.balance + v;
end
function Account:new(o)
local o = o or {};
setmetatable(o, self);
self.__index = self;
return o;
end
若想从这个类派出一个子类SpecialAccount,以方便顾客透支,则需要创建一个空的类,从基类中继承所有操作:
SpecialAccount = Account:new();--从基类继承
到现在,SpecialAccount还是一个Account的一个实例,下面我们可以为SpecialAccount
添加新方法和属性,代码如下:
s = SpecialAccount:new{limit = 1500.00} --在子类中添加新属性
function SpecialAccount:getLimit() --添加新的方法
return self.limit or 0;
end
分析:SpecialAccount从Account继承new,就像继承其它方法一样,但这次在执行new时,它的self参数表示是SecialAccount。因此s的元表为SpecialAccount,SpecialAccount中字段__index的值也是SpecialAccount。s 继承自SpecialAccount,而SpecialAccount又继承自Account。当执行
s:deposit(200);
Lua在s中找不到deposit字段时,就会查找SpecialAccount。如果仍找不到deposit字段,就查找Account。最终会在那里找到deposit的原始实现。
重写子类方法withdraw,代码如下:
function SpecialAccount:withdraw(v)
if v - self.balance > self:getLimit() then
error("insufficient funds");
endz
self.balance = self.balance - v;
end
分析:s:withdraw(2000);时,Lua就不会再Account中查找了。因为Lua会在SpecialAccount先找都withdarw方法。
多重继承
Lua中实现对重继承,关键在于用一个函数作为__index元字段。
那么,单继承与多重继承的差别也在这里,一个是只查找一个table,另一个是查找两个或以上的table。
我们就先来看看如何从2个或多个table中查找某个字段,如下代码:
function search(classes,key)
for i = 1, #classes do
local value = classes[i][key];
if value then
return value;
end
end
end
t1 = {name="张三"}
t2 = {add="南昌"}
print(search({t1,t2}, "name"));
分析:这里的classes参数,是一个table,这个table里又存放了多个table,也就是我们想要继承的那些类。
而key就是要查找的字段。
只需要遍历所有的table,判断这个字段是否在某个table里,找到之后,就返回这个值。
我们的测试代码就是从t1、t2中查找game这个字段,t1、t1可以看成是两个类。
运行结果:
张三
实现多继承
代码如下:
function createClasse(...)
local parents = {...};
local child = {};
setmetatable(child, {__index = function(table,key)
return search(parents, key)
end});
function child:new(o)
local o = o or {};
setmetatable(o, child);
child.__index = child;
return o;
end
return child;
end
createClass函数就是用来创建一个继承了多个类的子类,有点小复杂,慢慢分析:
1) 参数是一个可变参数,我们要将多个被继承的类作为参数传递进来
2) parents用于保存这些被继承的类
3) 创建一个新的table——child,它就是我们想要的那个继承了多个类的子类
4) 给child设置元表,并且设置__index元方法,__index元方法可以是一个函数,当它是一个函数时,它的参数就是元表所属的table,以及要查找的字段名。
5) 我们在__index元方法函数里调用search函数,从多个父类中查找所需的字段。于是,当调用child的某个函数时,就会从各个父类中查找,这已经完成了继承的工作了。
6) 接下来就是我们所熟悉的new函数,用来创建child的子类,实现方式和上一篇所说的是一样
7) 最后返回child,一切都完成了。
测试
代码如下:
-- 定义一只狗类
TSDog = {};
function TSDog:say()
print("这是一只狗类");
end
function TSDog:new(o)
local o = o or {};
setmetatable(o, self);
self.__index = self;
return o;
end
-- 定义一只猪类
TSPig = {};
function TSPig:work()
print("猪的惬意:吃-睡-吃...");
end
function TSPig:new(o)
local o = o or {};
setmetatable(o, self);
self.__index = self;
return o;
end
TSAnimation = createClasse(TSDog,TSPig);
local anim = TSAnimation:new()
anim:say();
anim:work();
运行结果:
这是一只狗类
猪的惬意:吃-睡-吃...
分析:
首先,Lua在TSAnimation 中无法找到字段"say"。因此,就查找anim元表中的__index字段,该字段为TSAnimation。由于在TSAnimation也无法提供字段"say"。然后Lua 查找TSAnimation元表__index字段,__index字段是一个函数,就会调用它,该函数则先查找会在TSDog中查找"say",发现找到了,就会返回"say"字段的值。若在TSDog没有找到"say"字段,就会接着遍历,在TSPig中查找。
私密性
Java、C++等语言,我们都很熟悉,public、private、protected等关键词。
这些关键词让封装成为了可能。Lua里是没有私密这种说法的,类也是一个table,table的所有字段都是可以调用的,并没有说哪些是公有的,哪些是私有的。那么如何在Lua中实现属性或者方法的私密性呢,请看下面代码:
function newAccount(initialBalance)
local self = {balance = initialBalance};
local withdraw = function(v)
self.balance = self.balance - v;
end
local deposit = function(v)
self.balance = self.balance + v;
end
local getBalance = function()
return self.balance;
end
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
这种做法的基本思想是,通过两个table来表示一个对象。一个table用来保存对象的状态,即属性。另一个table用于保存对象的操作,即方法。对象本身通过第二个table来访问。
下面来测试这个类,代码如下:
acc1 = newAccount(1000);
acc1.deposit(200);
print(acc1.getBalance());
运行结果:1200
弱引用table
Lua 采用了自动内存管理。一个程序只需创建对象,而无须删除对象。通过垃圾收集机制,Lua会自动地删除那些已成为垃圾的对象。但垃圾收集器只能回收那些它会认为是垃圾的东西,它不会回收那些用户认为是垃圾的东西,除非用户告诉Lua这项引用不应该阻碍此对象的回收,否则Lua无从得知该对象是否回收。而弱引用能告诉Lua一个引用不应该阻碍一个对象的回收。如果一个对象的所有引用都是弱引用,那么Lua就可以回收这个对象。
我们先前定义的table中的key和value都是强引用,它们会阻止对其引用对象的回收。弱引用table中,key和value都可以回收。在Lua中,有以下3种弱引用table:
1、具有弱引用key的table
2、具有弱引用value的table
3、同时具有两种弱引用的table
不论哪种类型的弱引用table,只要有一个key或者value被回收了,那么整个条目都会从table中删除。
创建弱引用table
一个弱引用类型是通过其元表中的__mode字段来决定的。这个字段值是个一字符串,该字符串若包含字母'k'这table的key是弱引用;若字符串包含字母'v',那么该table的value是弱引用。下面我们来创建一个key为弱引用的table,代码如下:
a = {};
b = {__mode = "k"};
setmetatable(a, b);
key = {};
a[key] = 1;
key = {};
a[key] = 2;
collectgarbage();
for i, v in pairs(a) do
print(i,v);
end
运行结果:
table: 0027C628 2
分析:在本例中,第二个赋值key = {}会覆盖第一个key,当收集器运行时,由于没有其他地方在引用第一个key,所以第一个key回收了,并且tabel中的相应条目也删除了。第二个key ,变量key 还在引用它,因此没有被回收。
注意:Lua只会回收弱引用table中的对象,是对象,像table中以数字,布尔值和string作为table的key或value,都不可回收,因为它们不是对象,从而也就不是弱引用。