参考教材:
https://www.runoob.com/lua/lua-tutorial.html(菜鸟教程)
http://www.lua.org/manual/5.4/(官方教程)
http://www.lua.org/pil/contents.html(Programming in Lua电子书)
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua的IDE:
IntelliJ IDEA插件EmmyLua、Chinar(下载地址-Emmylua)
LuaDist(windows-https://luadist.org/)
Windows->系统属性->环境变量->Path->新建->将LuaDist的bin加入。
成功添加环境变量后,cmd可以执行lua命名。
(参考文档:EmmyLua的安装与使用、https://emmylua.github.io/?plugin)
使用插件:EmmyLua。
安装插件:File->Settings->Plugins->⚙->Install Plugin from disk->选择.zip文件->重启IDEA。
创建工程:New Project->lua。
设置工程:File->Project Structure->SDKs->Sourcepath->添加lua文件所在目录->重启IDEA。
读取lua文件:File->Settings->Editor->File Types->新增Lua语言。
忽略meta文件:在上图中的Ignored Files and Folders中新增“*.meta”。
(参考文档:Lua 在VS上的环境配置(二)_陈言必行的博客-CSDN博客_lua vs)
1、生成lua.lib库
① 下载Lua源码,地址为Lua: download,Source板块下载。
② 打开VisualStudio,新建项目为静态库(.lib)的空项目。
③ 添加头文件:将Lua源码中的所有.h文件、.hpp文件添加为现有项。
④ 添加源文件:将Lua源码中的所有.c文件(除lua.c和luac.c)添加为现有项。
⑤ 编译为c代码:项目--属性--C/C++--高级--编译为--编译为C代码(/TC)。
⑥ 生成静态库:Release下生成--生成项目名,在项目文件夹的Release中生成.lib库。
2、调用lua.lib库
① 新建项目为控制台应用。
② 包含:项目--属性--C/C++--常规--附加包含目录--Lua源码的src目录。
③ 链接:项目--属性--链接器--常规--附加库目录--Lua源码的目录。
④ 依赖:项目--属性--链接器--输入--附加依赖项--Lua的.lib库。
3、测试
--- test.lua
print('Lua is OK!')
#include
#include
using namespace std;
int main()
{
lua_State* lua = luaL_newstate();
luaL_openlibs(lua);
luaL_dofile(lua, "test.lua");
lua_close(lua);
return 0;
}
Lua支持交互式编程(在命令行中编程)和脚本式编程(以.lua结尾的文件并执行)。
lua是动态类型语言,变量没有类型,只有值有类型。在lua中没有定义类型。
lua中所有值都是第一类值(first-class values),即所有值都可以存储在变量中,作为参数传入函数,或作为结果返回。
lua有8中数据类型:nil, boolean, number, string, function, userdata, thread, table。
nil |
该类只有一个值nil,表示一个无效值。 |
boolean |
false和true。(false和nil都可以表示条件否,主要不同在于false在table中是一个正常的键值,而nil表示键值不存在。) |
number |
整数或实数。number两个子类:integer和float,分别使用64bit整形和64bit双精度浮点。 |
string |
字符串,不可变的字节序列,可以包含任何8bit值。 不可以改变string中的一个字符,但可以创建新string。 包含转义字符,例如\n \t \v \\ \" \' \[ \] \f \a \b \r。 使用\ddd,可以将ASCII码转为字符,其中ddd是三位十进制数。 [[ ]]可以一段字符。 lua提供number和string运行时的自动转换,例如print("10" + 1) --> 11。 显示类型转换,使用函数tonumber、tostring等。 在Lua中,字符串的加法运算符用".."。 |
function |
由c或lua编写的函数。 |
userdata |
允许任的c数据存储,在lua中代表原始内存块,分为full userdata(由lua管理的一块内存的对象)和light userdata(一个c指针)。lua没有为userdata预定义操作,其值在lua中不能创建或修改,只能通过c的API。通过metatables可以为full userdata定义操作。 |
thread |
独立线程,用于实现协同程序。lua支持在所有系统上的协同程序,即时系统原生不支持。 |
table |
实现关联数组,数组的索引可以是除nil和NaN(not a number)外的所有值。如果索引是nil,则不是数组的一部分。function也可以作为索引和域(field)值。 table是lua中唯一的数据结构,可以构造数组、列表、集合、树、图等。 lua使用字段名作为索引,因此支持类似".name"作为a.["name"]的语法糖。 table的索引遵循raw equality,即只有i和j是raw equal时,a[i]和a[j]时同一个元素。注意的是,浮点型的整值将被转化为其代表的数的整形,即a[2.0]将被转化为a[2]。 一维数组:array = {'a', 'b', 'c', 1, 2, 3}(注意:Lua的默认索引从1开始) 二维数组:matrix = {{'a', 'b', 'c'}, {1, 2, 3}} |
function、userdata、thread和table类的值是对象,不包含值,只包含引用。分配、传参和函数返回只处理其引用。
Lua是一种自由形式的语言。它忽略词法元素(标记)之间的空格和注释,但作为两个标记之间的分隔符除外。在源代码中,Lua将标准ASCII空白字符空格、换行符、回车符、水平制表符和垂直制表符识别为空格。
以下单词保留,不能作为命名:
and break do else elseif end false for function goto if in
local nil not or repeat return then true until while
lua区分大小写。例如,and被保留,And和AND是另两个不同的词。
lua中避免以下划线_开头,后接大写字母的命名。
算符运算符:+ - * / % ^ // 加减乘除 取余 乘幂 整除(舍小数)
关系运算符:== ~= < > <= >= 等于 不等于 小于 大于 小于等于 大于等于
逻辑运算符:and or not
其他:.. 字符串加法 # 字符串/表的长度计算
Lua 变量有三种类型:全局变量、局部变量、表中的域。
单个命名可以表示全局变量或局部变量(包括函数的形参)。
Lua中的命名默认是全局变量,除非是明确声明为local(局部变量可由在其范围内定义的函数自由访问,作用域为从声明位置开始到所在语句块结束)。
Lua支持一组常规的语句,类似于其他常规语言中的语句,包括块、赋值、控制结构、函数调用和变量声明。
块是一个语句列表,按顺序执行:
block ::= {stat}
可以显式分隔块以生成单个语句:
stat ::= do block end
Lua有空语句,允许您用分号分隔语句、用分号开始一个块或按顺序写两个分号:
stat ::= ';'
Lua允许多赋值。在左侧定义变量列表,在右侧定义表达式列表,两个列表中的元素用逗号分隔:
stat ::= varlist ‘=’ explist
varlist ::= var {‘,’ var}
explist ::= exp {‘,’ exp}
在赋值前,将调整值列表长度与表达式列表一致,多余的表达式将被丢弃。如果表达式比变量少,将用nil扩充。
在多赋值时,一个变量同时被读取、赋值,Lua确保读取变量的值是在赋值之前。
if ... elseif ... else ... end条件语句如下:
stat ::= if exp then block {elseif exp then block} [else block] end
控制结构的条件表达式可以返回任何值。false和nil均表示为false。所有不同于nil和false的值均表示为true。数字0和空字符串也表示为true。
goto语句将控制程序转移到标签(Lua中的标签也被视为语句):
stat ::= goto Name
stat ::= label
label ::= ‘::’ Name ‘::’
标签在定义它的整个块中可见,嵌套函数内部除外。goto可以跳转到任何可见的标签,只要不进入局部变量的作用域。如果具有相同名称的标签可见,则不应声明标签,即使此其他标签已在封闭块中声明。
break语句终止while、repeat或for循环,跳转到循环后的下一个语句(结束最内部的封闭循环):
stat ::= break
return语句用于从函数或块(作为匿名函数处理)返回值,函数可以返回多个值:
stat ::= return [explist] [‘;’]
return语句只能写为块的最后一条语句。如果有必要在块的中间返回,那么可以使用显式内部块,就像习惯用法“do return end”一样,这样的话return是内部块中的最后一条语句。
Lua提高的循环结构包括:for(数值、泛型)、while、repeat。
数值for循环重复一段代码,同时控制变量进行算术运算:
stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end(for数字)
其中Name为控制变量,是循环体中的新的局部变量。循环首先对三个控制表达式求值一次,分别称为initial、limit和step。如果step为空,默认为1。如果step为0,将报错。每执行一次循环,initial加上一个step,当initial+step超出limit时,循环终止。只有当initial和step都是整数时,循环以整形执行;否则这三个值将被转换为浮点型,循环以浮点型执行(可能存在精度问题)。浮点型循环结束时,控制变量Name会溢出。
泛型for循环在迭代器函数上工作。在每次迭代中,迭代器函数都被调用以生成一个新值,当这个新值为零时停止:
stat ::= for namelist in explist do block end
namelist ::= Name {‘,’ Name}
namelist中为循环体的循环控制变量。循环首先计算表达式来生成4个值,分别为迭代器函数、状态、初始控制变量、结束值。对每个迭代,对控制变量和状态进行迭代器函数计算。如果控制变量为nil,循环结束。结束值的行为类似于待关闭变量,可以在循环结束时使用该变量释放资源。在循环中,不能改变控制变量的值。
(i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。)
while...do 循环语句在判断条件为 true 时会重复执行循环体语句:
stat ::= while exp do block end
repeat...until循环中,内部块不以until关键字结束,而是在条件之后结束,即在条件进行判断前循环体都会执行一次。因此,条件可以引用在循环块内声明的局部变量。
stat ::= repeat block until exp
Lua中,函数是对语句和表达式进行抽象的主要机制,可以进行一些特殊任务、计算并返回值。通常函数的定义如下:
——普通
functiondef ::= function funcbody
funcbody ::= ‘(’ [parlist] ‘)’ block end
——语法糖
stat ::= function funcname funcbody
stat ::= localfunction Name funcbody
funcname ::= Name {‘.’ Name} [‘:’ Name]
函数默认为全局函数,需要设置为局部函数,需要用local修饰。
调用函数时,argument需要写在括号(argu)里,没有参数需要写空括号()。但函数只有一个参数,且这个参数是string或table结构时,括号可以省略。
Lua程序使用的函数可以用Lua和C(或主机应用程序使用的任何其他语言)定义。当调用函数时,Lua中定义的函数和C中定义的没有功能上的区别。
parameter的工作方式与局部变量完全相同,使用函数调用中给定的argument进行初始化。可以使用与其parameter数量不等的argument调用函数。Lua根据param的数量调整argument的数量,就像在多重赋值中一样:多余的参数被丢弃;额外的参数为nil。可用于设置parameter的默认值,例如,一个参数para,首先进行para = para or 1的操作,当para为nil时,para的值为1。
Lua中的函数和其他值一样,都是匿名的。例如,提到print,更多的是指具有打印功能的名为print的变量。因此,可以像操作变量一样操作函数,例如赋值等。
函数定义实际上是一个将“function”类型的值分配给变量的语句(更具体地说是赋值)。
在上例中, function (x) ... end 是一个函数生成器,类似于table生成器,其产生的函数是匿名函数,可以将其赋值给一个function类型的变量。
Lua中,函数可以返回多个结果(Multiple Result),通过多赋值可以获取多个结果。
通过将需要返回的结果都列在return后,即可实现多返回值。
Lua会根据调用的情况调整函数的结果数。当我们将函数作为语句调用时,Lua会丢弃其所有结果。当我们使用调用作为表达式时,Lua只保留第一个结果。只有当调用是表达式列表中的最后一个(或唯一一个)表达式时,我们才会得到所有结果。这些列表出现在Lua中的四个构造中:多重赋值、函数调用的参数、表构造函数、返回语句。
在多重赋值中,函数调用作为最后一个(或唯一一个)表达式,会产生与变量数量匹配的结果。若函数返回值的数量不够,将产生nil。若函数不在最后一个,只会返回一个值。
当一个函数调用是另一个调用的最后(或唯一)参数时,第一个调用的所有结果都作为参数。如果我们写f(g())并且f有固定数量的参数,Lua将g的返回值数调整为f的参数个数。
构造函数将收集调用的多返回值函数的所有结果,无需任何调整。
哑变量(dummy variable):若多返回值的函数只能返回一个值,且需要除第一个返回值外的结果,此时可以通过哑变量来获取其他结果。
Lua中使用参数列表中的三个点(…)表示函数具有可变数量的参数。当调用此函数时,它的所有参数都收集在一个表中,该函数通过名为arg的隐藏参数访问该表。除了这些参数之外,arg表还有一个额外的字段n,其中包含实际收集的参数个数。
如果一个函数包含固定参数和可变参数,固定参数需要放在可变参数之前,Lua会将额外的参数全部分配给arg。定义如下:
function g (a, b, ...) end
当具有可变参数的函数需要将参数传递给另一个函数时,可以使用unpack(arg)作为参数调用另一个函数:解包将返回arg中的所有值,这些值将传递给另一个函数。
Lua中的参数传递机制是位置传递的:当我们调用函数时,参数根据其位置匹配参数。第一个argum为第一个parameter赋值,依此类推。Lua不支持为argument命名,但可以通过传入table作为唯一参数的方式,获得相同的效果。
官方文档:http://www.lua.org/pil/6.1.html。
Lua中的函数是具有适当词汇范围的第一类值。
在Lua中,函数是一个与数字和字符串等常规值具有相同权限的值。函数可以存储在变量(全局和局部)和表中,可以作为参数传递,也可以由其他函数返回。
函数可以访问其封闭函数的变量。(即Lua也包含lambda计算。)
高阶函数是将一个函数作为参数的函数。例如,函数table.sort,其参数是一个table,以及一个比较两值大小并返回true或false的匿名排序函数。
词法定界(lexical scoping):当一个函数被写在另一个函数中时,它有完全权限从封闭函数中访问局部变量。
在下例中,用于排序的匿名函数访问参数grades,而grades是封闭函数sortbygrade的局部变量。而在匿名函数的内部,grades既不是全局变量,也不是局部变量,而是外部局部变量,或称为upvalue。
闭包:闭包是一个函数加上正确访问其上值所需的所有内容(a closure is a function plus all it needs to access its upvalues correctly)。
下例中,如果我们再次调用newCounter,它将创建一个新的局部变量i,因此我们将得到一个新闭包,作用于该新变量。因此,c1和c2是同一函数上的不同闭包,每个闭包都作用于局部变量i的独立实例化。即在Lua中,闭包是value,而不是函数。
闭包在很多环境中提高了一个数值化工具,例如,高阶函数的参数、函数内构建函数、构建GUI界面的回调函数。
我们不仅可以将函数存储在全局变量中,还可以存储在table字段和局部变量中。
局部函数:在一个局部变量中存储function,该function被限制在给定的域中。
局部函数在包中很有用。Lua将每个块作为一个函数处理,块可以声明局部函数,这些函数仅在块内部可见。词法作用域确保包中的其他函数可以使用这些局部函数。
定义递归的局部函数时,需要注意局部函数是否已经定义。如果在调用时,局部函数还没有被定义,将尝试调用同名的全局函数,这就会出错。因此,必须首先定义局部变量,然后定义局部函数。
尾调用是一种类似调用的goto语法。当一个函数调用另一个函数作为其最后一个操作时,就会发生尾调用,因此它无需执行其他操作。例如,function f(x) return g(x) end,其中g就是一个尾调用。在f调用g之后,没有其他操作可做。在这种情况下,当被调用函数g结束时,程序不需要返回到调用函数f。因此,在尾调用之后,程序不需要在堆栈中保留任何有关调用函数f的信息。因此,Lua在尾调用时不需要使用任何的堆栈空间,也就被称为正确尾调用。
正确尾调用不需要使用任何的堆栈空间,因此没有尾调用的递归次数限制。在下例中,可以使用任何数作为参数,不用担心堆栈溢出。
如果在调用尾调函数后,函数还需要某些操作,那就不是正确尾调用的标准。如下几个都不是正确尾调用。
迭代器是允许对集合的元素进行迭代的任何构造。在Lua中,我们通常用函数表示迭代器:每次调用该函数时,它都会返回集合中的“下一个”元素。
闭包是从其封闭函数访问一个或多个局部变量的函数。这些变量在对闭包的连续调用中保持其值,从而允许闭包记住遍历过程中的位置。为了创建一个新的闭包,还必须创建它的外部局部变量。因此,闭包构造通常涉及两个功能:闭包本身和工厂(创建闭包的函数)。
下例是一个简单的迭代器iterator,只返回元素值。list_iter是工厂,每次调用将返回一个新的闭包。该闭包将其状态保持在其外部变量(t、i和n)中,因此,每次调用它时,它都会返回列表t中的下一个值。当列表中没有更多值时,迭代器将返回nil。分别用while和generic for进行了迭代。
generic for对迭代器的使用更加方便,从迭代循环中完成所有的记录:调用迭代器工厂;在内部保留迭代器函数,因此我们不需要iter变量;在每次新迭代时调用迭代器;当迭代器返回nil时停止循环。
之前的迭代器的一个缺点是,需要为每个新循环创建一个新的闭包。大部分情况下,创建新闭包的开销是可以忽略不计的。而当开销过大时,可以使用泛型for来保持迭代器状态。
泛型for在循环期间在内部保持迭代器函数。实际上保留三个值:迭代器函数、状态常量和控制变量。
泛型for的语法为:for
for所做的第一件事就是计算in之后的表达式。这些表达式应该得到for所保持的三个值:迭代器函数、状态常量和控制变量的初始值。与多重赋值一样,只有列表的最后一个(或唯一一个)元素可以产生多个值;并且将值的数量调整为三个,根据需要丢弃额外的值或添加nil。(在2.6.1中的简单迭代器,工厂只返回迭代器函数,状态常量和控制变量是nil。)
在这个初始化步骤之后,for使用两个参数调用迭代器函数:状态常量和控制变量。然后,for将迭代器函数返回的值分配给其变量列表中变量。如果返回的第一个值(分配给控制变量的值)为nil,则循环终止。否则,for将执行主体并再次调用迭代函数,重复该过程。(注意,对于for结构,状态常量根本没有意义。只是从初始化步骤中获取该值,并在调用迭代器函数时传递该值。)
泛型for语句 for var_1, ..., var_n in explist do block end,等价于以下while语句:
其中,f是迭代器函数,s是状态常量,控制变量var的初始值是a0。控制变量将在a1=f(s, a0), a2=f(s, a1), ...上循环,直到ai为nil。如果存在其他变量,只需要获取每次f的返回值。
无状态迭代器是一种自身不保持任何状态的迭代器。因此,可以在多个循环中使用相同的无状态迭代器,从而避免了创建新闭包的开销。在每次迭代中,for循环都使用两个参数调用迭代器函数:状态常量和控制变量。无状态迭代器仅使用这两个参数为迭代生成下一个元素。
ipairs是典型的无状态迭代器(用于迭代数组中的元素,例如for i,v in iparis(array) do print(i,v) end)。迭代的状态是正在遍历的表(状态常量)和当前索引(控制变量)。当Lua在for循环中调用ipairs时,将产生3个值:迭代器函数iter、状态常量a和控制变量的初始值0。然后Lua调用iter(a, 0),并获得1, a[1];第二个循环中,调用iter(a, 1),并获得2, a[2],直到nil元素。ipairs可以如下写出:
pairs函数也可以迭代table中元素,不同的是,其迭代器函数是next函数。在for循环中调用next(t, k),其中k是table t的键,返回table中的下一个键,同时返回该键对应的值。调用next(t, nil)将产生初始值(第一个pair),没有pair时next函数返回nil。
当迭代器需要保持多个状态,超出一个状态常量和一个控制变量时,可以使用闭包,或者,将所有需要的内容打包到一个table中,并将该table用作迭代的状态常量。迭代器可以在循环中通过table来保所需数据,并随着数据变化而变化。尽管table是状态常量,但其内容可以改变。由于所需的全部数据都在table中,因此会丢弃for循环的第二个参数。
下例为一个输出txt文本所有单词的迭代器:
local function iterator (state) ---iterator
while state.line do -- repeat while there are lines
local s, e = string.find(state.line, "%w+", state.pos) -- next word
if s then
state.pos = e + 1
return string.sub(state.line, s, e)
else
state.line = io.read() -- next line
state.pos = 1
end
end
return nil -- no more lines: end loop
end
function allwords ()
local state = {line = io.read(), pos = 1} ---table
return iterator, state
end
闭包通常比使用表的迭代器更高效、更优雅:1、创建闭包比创建表的开销少;2、访问upvalue比访问table字段更快。
以下是上例allwords()函数的闭包形式:
function allwords ()
local line = io.read()
local pos = 1
return function () --- iterator (closure)
while line do -- repeat while there are lines
local s, e = string.find(line, "%w+", pos) -- next word
if s then
pos = e + 1
return string.sub(line, s, e)
else
line = io.read() -- next line
pos = 1
end
end
return nil -- no more lines: end of traversal
end
end
之前的迭代器并没有在迭代,而是for循环在迭代。迭代器只是提供了迭代过程所需的连续的值,因此更像是一个生成器generator。
真正的迭代器不需要循环,而是通过一个参数告诉迭代器每一次迭代时的操作,即迭代器接受一个函数作为参数,在每次循环时调用。
下例重写了allwords()函数以及调用:
function allwords (f)
for l in io.lines() do -- each line
for w in string.gfind(l, "%w+") do -- each word
f(w) --- call the function
end
end
end
allwords(print) --- use the itertor
local count = 0 --- use the itertor (anonymous function)
allwords(function (w)
if w == "hello" then count = count + 1 end
end)
print(count)
生成器式的迭代器调用匿名函数(写法类似):
local count = 0
for w in allwords() do
if w == "hello" then count = count + 1 end
end
print(count)
协同程序类似于线程(在多线程意义上):具有自己的堆栈、自己的局部变量、自己的指令指针;但与其他协程共享全局变量和大部分其他内容。
协程和线程之间的主要区别在于,具有线程的程序同时运行多个线程。协程是协作的:具有协程的程序在任何给定的时间都只运行其中一个协程,而这个运行的协程只有在明确要求挂起时才暂停执行。
Lua所有的协程函数都在coroutine表中。
coroutine.create() |
作用:创建一个协程,初始是suspended状态。 参数:一个function(函数,可以用匿名函数传参)。 返回:一个thread类(协程)。 |
coroutine.status() |
作用:协程有三种状态:suspended、running、dead可查询状态。 参数:一个thread类(协程)。 返回:一个string(协程的状态)。 |
coroutine.resume() |
作用:开始/重新开始一个协程,从suspended改为running。Lua的resume以安全模式运行,发生error时将返回false加error信息。 参数:一个thread类(协程)。 返回:boolean(suspended态为true,dead为false)。 |
coroutine.yield() |
作用:将正在运行的协程暂停,以便稍后恢复。 参数:Any。(与resume配合使用) 返回:。 |
coroutine.warp() |
作用:创建一个coroutine,以function的形式,类似于create()。调用时出错将抛出error,而不是像resume那样返回错误码。 参数:一个function(Lua的匿名参数)。 返回:一个function(调用该函数时即resume该协程)。 |
coroutine.running() |
作用:返回当前的线程和判断是否是主线程。 返回:一个thread和一个boolean。(主线程返回true) |
在Lua中,一对resume和yield可以交换数据。主要用途包括:
① 若一个resume没有相应的yield,它将全部额外参数传递给主函数协程。
② 一个对resume的调用将返回的全部额外参数传递给相应yield。
③ 同样地,yield将返回的全部额外参数传递给相应resume。
官方文档:http://www.lua.org/pil/9.2.html。
Lua的协程yield-resume来模拟生产者-消费者关系:
local newProductor
function productor() --- 生产者
local i = 0
while i < 10 do
i = i + 1
coroutine.yield(i) --- 发送
end
end
function consumer() --- 消费者
local status, v = {true, ''}
while status do
status, v = coroutine.resume(newProductor) --- 接收
print(v)
end
end
newProductor = coroutine.create(productor)
consumer()
--- 1 2 3 4 5 6 7 8 9 10 nil
生产者-消费者的问题主要是如何匹配发送和接收,即谁有主循环。生产者和消费者都是激活的,都有自己的主循环,并且都假设另一个是可调用的。Lua中的resume-yield颠覆了典型的调用者-被调用者关系,是解决生产者-消费者问题的有力工具。当协程调用yield时,不会进入函数,而是被挂起(等待resume调用)。类似地,调用resume时,也不会进入函数,而是返回yield的调用。这样,消费者resume生产者使之产生一个新值,生产者yield新值返回消费者(即循环的跳出通过resume、yield来完成)。上例是消费者驱动的,当消费者需要一个物品时,它会resume生产者,生产者会一直运行,直到有一个物品要送给消费者,然后停止,直到消费者再次启动它。
过滤器是位于生产者和消费者之间的任务,对数据进行某种转换。既是生产者,又是消费者,resume一个生产者产生值,又yield一个转换后的值给消费者。
在上例中,增加一个过滤器,判断产生的数字的奇偶性:
local newProductor
function productor() --- 生产者(也可以写成返回一个thread类,如filter())
local i = 0
while i < 10 do
i = i + 1
coroutine.yield(i)
end
end
function filter() --- 过滤器
return coroutine.create(function()
local result = ''
local status, v = true, ''
while status do
local status, v = coroutine.resume(newProductor) --- 消费
if v % 2 == 0 then result = 'even' else result = 'odd' end --- 过滤
coroutine.yield(v..':\t'..result) --- 生产
end
end)
end
function consumer() --- 消费者
local status, v= true, '', ''
while status do
status, v= coroutine.resume(filter()) --- 消费(过滤器生成的值)
print(v)
end
end
newProductor = coroutine.create(productor)
consumer()
管道与协程对比:在管道中,每个任务都在一个单独的进程中运行;而在协程中,每一个任务都在单独的协程中运行。在进程之间切换的成本很高。管道在生产者和消费者之间提供缓冲,因此它们的相对速度有一定的自由度。而使用协程,在任务之间切换的成本要小得多(大致与函数调用相同),因此生产者和消费者可以携手共进。
迭代器可以产生被循环体消费的物品,因此,可以使用协程来写迭代器。
下例是一个生成数组后三行的迭代器:
function next3line(a) --- 协程任务
local n = table.getn(a)
for j=0,3 do
for i=1,n do a[i] = a[i] + j end
coroutine.yield(a)
end
end
function iter_next3line(a) --- 迭代器
local co = coroutine.create(function() next3line(a) end)
return function()
local status, value = coroutine.resume(co)
return value
end
end
for arr in iter_next3line({1,2,3,4,5,6}) do --- 使用迭代器
for i,v in ipairs(arr) do io.write(v, " ") end
io.write("\n")
end
其中iter_next3line迭代器是Lua中的常见模式,将一个协程与调用其的resume打包到一个函数中。利用Lua中的coroutine.wrap()函数,还可以将iter_next3line()迭代器写成如下形式:
function iter_next3line(a)
return coroutine.wrap(function() next3line(a) end) --- wrap函数
end
Lua作为一种解释性语言,在运行源代码之前,Lua总是将源代码预编译为中间形式。解释性语言进行编译可能不合理,但是,解释性语言的显著特点并不在于它们不是编译的,而是任何编译器都是语言运行时的一部分,因此,执行动态生成的代码是可能的(而且很容易)。类似dofile函数的存在使得Lua被称为解释性语言。
dofile():打开命名文件并将其内容作为Lua块执行,没有参数时执行标准输入的内容,返回执行文件返回的所有值。出错时,将错误抛出该调用者。
loadfile():将命名文件的内容或标准输入内容(没有参数时)编译为函数,返回该函数。出错时,返回nil和错误码。
--File:test.lua --- 文件
--function func(a,b)
-- print('defined!')
-- return a+b
--end
loadfile('test.lua')() --- 编译,同时调用编译后返回的函数
func(10,20) --- 此时可调用已定义的func()
--- 相当于 ---
f = function()
function func1(a,b)
print('defined!')
return a+b
end
end
f() --- 调用编译后返回的函数
func1(2,3)
loadstring():与loadfile()类似,但是从string读取代码块,并编译为函数,返回该函数。出错时,返回nil和错误码。
--- 正确
f = loadstring('local a=5; b=10; return a + b')
print(f()) --- 15
--- 错误
print(loadstring('local a=5; b=10; return a + ')) --- nil unexpected symbol near ''
assert():如果参数为nil或false,则抛出错误。否则,返回其全部参数。当使用loadfile或loadstring时,可以用它抛出错误。
s = 'local a=5; b=10; return a + '
print(loadstring(s)) --- nil unexpected symbol near ''
f = assert(loadstring(s)) --- raise an error
require函数是Lua提供的一个高级函数来加载和运行库,是Lua中加载库的首选函数。与dofile的区别:① 在路径中搜索文件;② 要求控制文件是否已运行,以避免重复工作。
Lua运行的虚拟平台ANSI C没有目录的概念,所以require函数使用的路径不同于典型路径(搜索给定文件的目录列表)。因此,require使用的路径是一个模式列表,每个模式都指定了将虚拟文件名(require的参数)转换为真实文件名的替代方法。具体说,路径中的每个元素都是包含可选询问标记的文件名。对于每个元素,require用虚拟文件名替换每个' ? ',并检查是否存在具有该名称的文件;如果没有,则转到下一个元素。路径中的元素用分号' ; '分隔。例如,?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua。
如果一个元素没有问号(即固定的文件名),例如,?;?.lua;/usr/local/default.lua。这种情况下,当require函数找不到其他文件时,将会运行该固定文件。
因为Lua与C的接口很容易,所以用C编写Lua的包也很容易。C包需要在使用前加载并与应用程序链接。然而,ANSI C不支持动态链接。因此,在这种特殊情况下,Lua打破了其兼容性规则,并使用条件代码为多个平台实现了动态链接功能。标准实现为Windows(DLL)、Linux、FreeBSD、Solaris和一些其他Unix提供了这种支持。(在lua命令行运行print(package.loadlib()),如果报错为错误参数,则安装了动态链接功能。)
package.loadlib()提供了动态连接功能,两个string参数:库的完整路径和初始化函数的名称。它将初始化函数作为Lua函数返回,以便我们可以直接从Lua调用该函数。如果出错,将返回nil和错误码。
local path = "/usr/local/lua/lib/libluasocket.so"
local func = package.loadlib(path, "luaopen_socket")
然后,为了安装库,我们将实际的二进制共享库放在任何位置,编辑存根以反映真实路径,然后将存根文件(stub file)添加到LUA_path中的目录中。通过此设置,我们可以使用常规的require函数打开C库。
Lua是一种扩展性语言,经常嵌入到应用程序中,所以一般不会在发生错误时简单地崩溃或退出。相反,每当发生错误时,Lua都会结束当前块并返回到应用程序。
通过error函数可以显式地抛出错误,无返回值。参数message是string类的错误码,另一个参数level是错误位置,1(默认值)代表错误位置是error函数调用的位置,2代表错误位置是调用error的函数的位置,0代表不报告错误位置。
通过assert函数可以代替上例中if条件的功能,其参数v值为nil或false时抛出错误。第二个可选参数message是错误码,默认值为"assertion failed!"。当第一个参数v不是nil或false时,返回该参数。
对于许多应用程序,错误处理由该程序执行,不需要在Lua中进行任何错误处理。所有Lua活动都从应用程序的调用开始,通常要求Lua只运行一个代码块。如果出现任何错误,此调用将返回错误代码,应用程序可以采取适当的处理。对于独立解释器,其主循环只打印错误消息,并继续显示提示并运行命令。
Lua中的错误处理是通过pcall函数来封装代码。pcall函数的参数是一个函数,因此需要将代码封装在一个函数(或匿名函数)中。pcall的返回值有两个(boolean和result),如果第一个返回是true表示调用正确运行,则第二个返回是调用函数的全部返回值;如果是false,则第二个返回是错误对象。
Lua的异常处理机制:用error函数抛出一个异常,用pcall函数捕获它。错误消息标识类型或错误。
Lua中用table表示所有的数据结构,数组、记录、列表、队列、集合等都用table表示。通过使用table,许多算法被简化到极致。
数组 |
初始化数组:arr = {}。(起始索引可以是任意整数,0、1或负数,默认为1。) 构造器创建数组:array = {1,2,3,4,5,6} |
矩阵/ 多维数组 |
在Lua中构造矩阵:方法一:构造一个数组的数组;方法二:将多维索引合并成一维(即用数组表示矩阵,例如2行3列为2:3,因为Lua中table是有索引的)。 table是天然稀疏的,只有非nil值需要空间,nil值不浪费内存。 |
链表 |
初始化链表:list = nil。 头插法:list = {next = list, value = v} 遍历链表:local l = list; while l do print(l.value); l = l.next end |
队列/ 双队列 |
避免污染全局空间,将方法存储在一个table中,例如Queue库:Queue={}。 构造方法:function Queue.new() return {first=0, last=-1} end 头插法(尾插法类似): function Queue.pushleft(list, value) local first = list.first - 1 list.first = first list[first] = value end 头删法(尾删法类似): function Queue.popleft(list) local first = list.first if first > list.last then error("list is empty") end local value = list[first] list[first] = nil --- garbage collection list.first = list.first + 1 return value end |
集合 |
构造方法: function Set (list) local set = {} for _, l in ipairs(list) do set[l] = true end return set end reserved = Set {"while", "end", "function", "local", } ———————— reserved = { ["while"] = true, ["end"] = true, ["function"] = true, ["local"] = true, } |
元表metatable是Lua中普通的表,但其中包含了原始值行为的定义,Lua中每个值都可以有一个元表。通过修改元表中的特定字段,可以改变该值的某个行为。元表中的键是以双下划线"__"开头加操作名的字符串,对应的值称为元值metavalue,大部分元值必定是函数,也被称为元方法metamethod。例如,为table对象进行加法操作,Lua会在table对象的元表中查找__add,如果找到了,将会调用__add对应的元方法执行加法操作。
getmetatable函数可以查询任意值的元表。getmetatable(value),如果有则返回元表的__metatable字段的值(若有)或元表,否则返回nil。
setmetatable函数可以为table类型设定元表。setmetatable(table, metatable)。
table、userdata可以有独立的元表,也可以多个table或userdata共享元表。其他类型如number、string等,都是一个类型有一个元表。一个值默认没有元表,除table类型外的其他类型,只能通过debug库来设置元表。string库可以为string类型设置元表。
元表可以控制的操作:
__add |
加法(+)运算 |
若加法的任一个操作数不是number,Lua将尝试调用元方法。按先后顺序查找两个操作数的__add的元方法。若找到,则调用该元方法,并将两个操作数作为参数,返回结果;否则抛出错误。 |
__sub |
减法(-)运算 |
与加法运算相似。 |
__mul |
乘法(*)运算 |
与加法运算相似。 |
__div |
除法(/)运算 |
与加法运算相似。 |
__mod |
模(%)运算 |
与加法运算相似。 |
__pow |
幂(^)运算 |
与加法运算相似。 |
__unm |
否(一元-)运算 |
与加法运算相似。 |
__idiv |
整除(//)运算 |
与加法运算相似。 |
__band |
按位AND(&)运算 |
与加法运算相似。当操作数既不是整数也不是可强制转为整数的浮点数,Lua将调用元方法。 |
__bor |
按位OR(|)运算 |
与按位AND运算相似。 |
__bxor |
按位异或(二元~)运算 |
与按位AND运算相似。 |
__bnot |
按位NOT(一元~)运算 |
与按位AND运算相似。 |
__shl |
按位左移(<<) |
与按位AND运算相似。 |
_shr |
按位右移(<<) |
与按位AND运算相似。 |
__concat |
串联(..)运算 |
与加法运算相似。当操作数既不是string也不是number(默认强制转为string),Lua将调用元方法。 |
__len |
长度(#)运算 |
如果对象不是string,Lua将调用元方法。如果没有元方法但对象是个table,将调用table的长度运算。否则,抛出错误。#str |
__eq |
等于(==)运算 |
与加法运算相似。仅当比较的值是两个table或两个full userdata并且它们最初不相等时,才会尝试元方法。调用的结果始终转换为布尔值。 |
__lt |
小于(<)运算 |
与加法运算相似。仅当比较的值两个不全是number或string时,才会尝试元方法。调用的结果始终转换为布尔值。 |
__le |
小于等于(<=)运算 |
与小于运算相似。 |
__index |
索引存取操作t[key] |
当t不是一个table或key不在t中时,发生index事件。在t的元表中查找元值。该事件的元值可以是函数、表或带有__index元值的任意值。若是函数,则将t、key作为参数,返回结果;否则返回key索引元值的结果。 |
__newindex |
索引赋值t[key]=v |
与index事件类似。若是函数,则将t、key、v作为参数,返回结果;否则,Lua重复使用相同的键和值对该元值进行索引赋值。 |
__call |
调用obj(args) |
当Lua尝试调用非函数的值时,发生该事件。元方法在objs中查找。如果存在,则使用objs作为第一个参数调用元方法,然后是原始调用的参数(args)。将返回所有的结果。只有元方法允许多返回值。 |
__gc |
垃圾回收元方法(finalizer) |
用于table和userdata的垃圾回收。当垃圾回收器检测到table或userdata失效时,终结器允许协同Lua的垃圾收集与外部资源管理,如关闭文件、网络或数据库连接、释放内存。 |
__close |
关闭待关闭变量 |
当关闭一个值时,调用该元方法,第一个参数为值本身,第二个参数是error对象或nil。(待关闭变量: 待关闭变量的行为类似于常量局部变量,但每当变量超出作用域时,其值都是关闭的,包括正常块终止、通过break/goto/return退出其块或因错误退出程序。 赋值给待关闭变量的值必须具有__close元方法或为false值。 若多个待关闭变量超出作用域时,按其声明顺序的逆序关闭。 闭包方法发生错误,与其他常规代码一样处理。 如果协程不再resume或出错,可以使用finalizer或coroutine.close关闭变量。若是wrap创建的协程,出错时其函数会关闭协程。) |
__mode |
控制table中键和值的弱引用 |
其元值“k”代表弱键,“v”代表弱值,“kv”代表弱键弱值。 (弱表:其元素是弱引用的表。垃圾收集器将忽略弱引用。即,如果某对象的唯一引用是弱引用,那么垃圾收集器将回收该对象。一个弱值的表,将允许回收其值;弱键弱值的表,将允许回收其键值。在任何情况下,如果回收了键或值,则整个键值对将从表中删除。瞬表(ephemeron table)是弱键强值的表,只有值的键可达时,该值才可达。如果键的唯一引用是值,则将该键值对删除。对表的__mode修改只在下一个垃圾回收期生效,即修改后仍有可能被回收。只有具有显式构造的对象才会从弱表中删除。值(如number、轻C function或行为类似值的string)不受垃圾回收的影响,因此不会从弱表中删除。 |
为用table模拟的List类增加一个__add的元方法。
List = {} --- store methods
function List.new(t) --- List constructor
local list = {}
setmetatable(list, List.mt) --- set mt as List's metatable
for i, v in ipairs(t) do list[i] = v end
return list
end
List.mt = {__add = function (a,b) --- table as metatable
local result, len = {}, table.getn(a) --- '__add' field
for i,v in ipairs(a) do result[i] = v end
for i,v in ipairs(b) do result[len + i] = v end
return result
end}
a, b = {5,6,7,8}, List.new{1,2,3,4}
c = a + b --- any one having mt is OK
for _,v in ipairs(c) do print(v) end --- 5,6,7,8,1,2,3,4
Lua中只有__eq、__le、__lt三种关系运算符的元方法,其他三种会被转化:a~=b转换为not (a==b),a>b转换为b=b转换为b<=a。
为List增加一个比较长度的运算符:
List.mt.__le = function(a,b) --- '__le' field
if table.getn(a) <= table.getn(b) then
return true
end
end
List.mt.__lt = function(a,b) return a<=b and not(b<=a) end --- '__lt' field
a, b = List.new{5,6,7,8}, List.new{1,2,3,4} --- all must have mt
print(a<=b)
print(a
Lua中print()函数总是调用tostring()来格式化输出。通过为元表中的__tostring字段增加元方法,List可以直接打印。
List.mt.__tostring = function(t) --- '__tostring' field
local str, sep = '{', ''
for _,v in ipairs(t) do
str = str..sep..v
sep = ', '
end
return str..'}'
end
b = List.new{5,6,7,8}
print(b)
setmetatable() / getmetatable()函数是修改/获取元表中的__matatable字段的元值来完成元表的操作,通过为__matatable设置一个值来保护List的元表,避免被修改/看到。
List.mt.__metatable = 'not your business'
l1 = List.new({})
print(getmetatable(l1)) --- ->not your business
setmetatable(l1, {}) --- Error: cannot change a protected metatable
Lua还提供了一种方法__index,可以在正常情况下更改表的行为,即查询和修改表中缺少的字段,不同于算术、关系运算的元方法(这两个是执行表未定义行为时触发的,即操作出错时调用)。
__index的元方法:当尝试获取表中不存在的字段时,调用__index的元方法,该元方法将返回结果(无论什么结果),而不是得到nil。
使用__index的元方法可以实现继承,从原型中获取不存在的字段。
Window = {prototype = {x=0, y=0, width=100, height=100},
new = function(o)
setmetatable(o, Window.mt)
return o
end,
mt = {__index=function(table, key) return Window.prototype[key] end}
}
w1 = Window.new({x=10, y=20})
print(w1.width) --- ->100
当Lua检测到w1没有width属性但元表中有__index时,Lua将调用__index的元方法,参数为table: w1(操作的表)和key: width(缺失的键)。元方法用给定的键width在原型Window中查找并返回。
在Luau中,__index的元方法很常用,可以将函数简化为一个表,执行的动作与函数相同。即上述的可以简化为:
Window = {prototype = {x=0, y=0, width=100, height=100},
new = function(o)
setmetatable(o, Window.mt)
return o
end,
mt = {}
}
Window.mt.__index = Window.prototype --- 注意变量定义的顺序
--- OR ---
Window = {new = function(o)
setmetatable(o, Window.mt)
return o
end,
mt = {__index = {x=0, y=0, width=100, height=100}}
}
rawget(t, i)函数可以绕过元方法,对表t进行原始访问,而不调用元方法__index。
__newindex的元方法会更新__index的元方法对表的访问操作。当为缺失的索引赋值时,Lua会查找并调用__newindex的元方法,而不是执行赋值。如果元方法是个table,将在元方法的表中赋值,而不是在原表中。
rawset(t, k, v)函数可以绕过元方法,对原表t的索引k赋值为v,而不调用元方法__newindex。
__index和__newindex仅在表中不存在索引时才会关联。因此可以为某个表设置一个空table作为代理,来检测该表的访问历史。只有空table才能够检测所有访问并重定向到原始表。如果要将多个原始表的多个代理共享一个通用的元表,可以将原始表保存在代理的一个字段中,且要保证这个字段不会用于其他目的(可以给这个字段设置一个私有索引)。
local index = {} --- private index
local mt = { --- create metatable
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k] --- access the original table
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
t[index][k] = v --- update the original table
end
}
function track (t) --- Tracking Table Accesses
local proxy = {} --- create proxy
proxy[index] = t --- associate originalTable to its proxy
setmetatable(proxy, mt) --- set a common metatable
return proxy
end
t = { 1, 2, 3, 4, 5} --- original table
t = track(t) --- monitor the original table
t[5] = 'hello' --- -> *update of element 5 to hello
print(t[5]) --- -> *access to element 5 ->hello
print(t[index][1]) --- -> 1 (access the original table)
将__newindex的元方法改为抛出错误,就可以将原始表模拟成只读属性,同时将__index指向原始表来访问原始表。
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
table = readOnly({1,2,3,4,5})
print(table[1]) --- -> 1
table[2] = 20 --- Error: attempt to update a read-only table
Lua中的表是一个不止意义上的对象。与对象一样,表具有状态,表具有独立于其值的身份。与对象一样,表的生命周期与创建它们的人或创建它们的位置无关。
定义一个对象(Account)。
Account = {balance = 0}。
定义一个方法。
function Account.withdraw (self, v) self.balance = self.balance - v end。
通过定义一个额外参数self,作为操作的接收者,使方法可以作用于全部同类的对象,而不是只作用于存储该方法的特定对象。
a2 = {balance=0, withdraw = Account.withdraw}
a2.withdraw(a2, 260.00)
self参数的使用是任何面向对象语言的中心。大多数OO语言都部分隐藏了这个机制,因此不必声明这个参数(但在方法中仍使用self或this)。Lua可以使用“:”运算符隐藏该参数。
function Account:withdraw (v)
self.balance = self.balance - v
end
a:withdraw(100.00) --- 调用方法
冒号“:”的作用是在方法定义中添加额外的隐藏参数,并在方法调用中添加额外参数。冒号只是一种句法工具。只要正确处理额外参数,也可以用“.”点语法定义函数,并用冒号语法调用它,反之亦然。冒号“:”仅仅是语法糖。
Account = { balance=0,
withdraw = function (self, v)
self.balance = self.balance - v
end
}
function Account:deposit (v)
self.balance = self.balance + v
end
Account.deposit(Account, 200.00)
Account:withdraw(100.00)
Lua中没有类的概念,但能遵循基于原型的语言,来模拟类。原型是一个规律对象,是一个对象查找其未定义行为的地方。类和原型都是存放由多个对象共享的行为的地方。Lua中只需要创建一个对象,专门用作其他对象(其实例)的原型。
在Lua中,创建原型只需要使用setmetatable(a, {__index = b}),即将b作为a的原型。a可以在b中查找其没有的任何操作。此时,b可以看作是对象a的类。使用__index元方法让新对象从类中继承操作。可以据此为Account类定义一个构造函数(用Account的表作为新对象的原表):
Account = { balance=0,
withdraw = function (self, v)
if v > self.balance then error("insufficient funds", 2) end
self.balance = self.balance - v
end
}
function Account:new (o)
o = o or {} --- create object if user does not provide one
setmetatable(o, self)
self.__index = self --- 将Account本身作为元表
return o
end
a = Account:new{balance = 1000} --- create a new account
a:withdraw(100) --- 等同于a.withdraw(a, 100)
在上例中,a将Account作为其元表,调用a:deposit(100.00),其实是调用a.deposit(a, 100.00)。当在a中找不到deposit()时,将会查看元表的__index项,即getmetatable(a).__index.deposit(a, 100.00)。a的元表是Account,Account.__index也是Account。因此Account.deposit(a, 100)有同样的效果。这是Lua调用了deposit()函数,将a作为了self参数,因此a继承了Account的deposit()函数。Account中的其他字段同理。当再次调用a的deposit()时,索引元方法不再引用,因为a已经有了deposit()方法。
在Lua中由基类衍生一个子类可以简单地写为:
SpecialAccount = Account: new() --- 此时只是Account地一个实例
s = SpecialAccount: new{limit=1000.00} --- 子类
SpecialAccount从Account继承了new()函数,self参数是SpecialAccount,因此s的元表是SpecialAccount。Lua在s中找不到某方法,所以在SpecialAccount中找,再找不到,就去Account中找。
重写方法只需要重定义从父类继承的方法,这使得子类SpecialAccount与父类Account不同,例如:
function SpecialAccount:withdraw (v) --- redefine
if v - self.balance >= self:getLimit() then error"insufficient funds" end
self.balance = self.balance - v
end
function SpecialAccount:getLimit ()
return self.limit or 0
end
s:withdraw(100)
此时调用withdraw()方法,Lua不再去Account中找,因为SpecialAccount中有新的withdraw()。
Lua中,不需要为某一个对象的某一个操作特意定义一个新类,可以直接对该对象进行重定义,调用时将引用最新的定义。例如,对对象s的getLimit()方法进行重定义:
function s:getLimit ()
return self.balance * 0.1
end
Lua中面向对象不是原生的,因此有多种方式进行面向对象编程。使用__index元方法是最简单、灵活的。其他方式可以用于一些特定情况,例如多继承。
类不能同时是其实例的元表和自身的元表。而多继承是一个子类有多个父类,因此不能用类的方法去创建子类。只能通过一个特殊的函数CreateClass()来实现,其参数是新类的几个父类。该函数创建一个新表来代表类,其元表的__index元方法来完成多继承。因此多继承中父类和子类的关系不同于类和实例。
下例是多继承的一个构造方法:
local function search (k, plist) --- search for each method of its all parents
for i=1, table.getn(plist) do
local v = plist[i][k]
if v then return v end
end
end
function createClass (...)
local c = {} --- new class
setmetatable(c, {__index = function (t, k)
return search(k, arg)
end})
c.__index = c --- the metatable of its instances
function c:new (o) --- constructor
o = o or {}
setmetatable(o, c)
return o
end
return c --- return new class
end
创建一个多继承的子类:
NamedAccount = createClass(Account, Named) --- 继承于Account、Named
account = NamedAccount:new{name = "Paul"}
当在account中找不到name字段,转到account元表NamedAccount的__index()字段查找,即调用该字段对应的函数,便在Account和Named中查找。
为提高搜索父类的性能,可以将继承的方法复制到子类中。这可以使调用多继承方法像成员方法一样块,但缺点是不能随继承链传播。
...
setmetatable(c, {__index = function (t, k)
local v = search(k, arg)
t[k] = v -- save for next access
return v
end})
...
Lua中也没有私有机制,虽然私有被认为是面向对象的主要部分,每个对象的状态只是该对象的内部事务。Lua语言的设计目的也只是中小型程序,或嵌入大型程序,因此Lua避免冗余的、人为的限制。如果不相对某个对象做某个操作,就不做。然而,Lua是灵活的语言,可以模拟权限控制的机制。
Lua中实现权限控制,是通过两个表来代表一个对象,一个表存储对象状态,另一个表存储对象操作(接口)。对象本身通过第二个表访问,即通过组成其接口的操作访问。为了避免非法访问,表示对象状态的表不保存在另一个表的字段中,只保留在方法的闭包中。
下例是为之前的Account类添加权限控制:
function newAccount (initialBalance)
local self = {balance = initialBalance} --- 1.create a table to keep internal state
local withdraw = function (v) --- 2.create closure for methods
self.balance = self.balance - v --- access 'self' directly
end
local getBalance = function ()
return self.balance
end
return { --- 3.return external object (interface)
withdraw = withdraw, --- actual method implementations
getBalance = getBalance
}
end
acc = newAccount(100.00)
acc.withdraw(40.00) --- 不需要冒号“:”语法
print(acc.getBalance()) --- 60
print(acc.balance) --- nil
定义私有方法只需要不把方法放在接口中,而是通过公共方法去调用。
Lua有自动的垃圾回收系统,可避免空指针和内存泄漏等内存管理问题。
Lua不能自动识别某些内存垃圾。例如在堆栈(用数组实现)中,若通过简单的删除一个对象来减小堆顶,数组中剩余元素对Lua来说不是垃圾。在Lua中,任何存储在全局变量中的对象都不是垃圾,即时不再使用,此时需要为这些量赋值nil。若在程序中,把所有对象都存储在集合中,Lua不知道这个引用会阻碍垃圾回收系统,因此这些对象都不会被回收(除非指明)。
弱表就是一个为Lua指明某些引用不能阻止对象回收的机制。弱引用是被垃圾回收器忽略的对该对象的引用。如果指向某个对象的引用都是弱引用,这个对象将被回收,这些弱引用也被删除。弱表就是一个所有引用都是弱引用的表。表键和值都可以引用某个对象,一般情况下都是强引用,不会被回收。弱表有三种:弱键、弱值、弱键弱值。当键或值被回收,这个键值对就被回收。根据元表中的__mode字段来设置弱表的种类。
t = {}
b = {__mode='k'}
setmetatable(t, b) --- metatable with weak keys
key = {}
t[key] = 1 --- key refers to {}
key = {}
t[key] = 2
collectgarbage() --- forced collectgarbage
for k,v in pairs(t) do print(k,v) end --- -> table: 026DA640 2
————————————————————
--- result of normal table:
--- -> table: 007FA370 1
--- -> table: 007FA258 2
表t是个弱键的弱表,第二次key={} 赋值重写了第一次的key,使第一个键{}没有了key的强引用,只剩下值对键的引用,在垃圾回收期内被删除。
只有对象会被回收,值(number、boolean)是不可回收的。字符串从程序员角度是值,也不会被回收。
弱表应用之一:自动清理缓存。对于用空间换时间的Memoize Functions,可以定时清理缓存,避免内存耗尽。
local results = {}
setmetatable(results, {__mode = "v"}) --- weak values
function createRGB (r, g, b) --- an expensive function
local key = r .. "-" .. g .. "-" .. b
if results[key] then return results[key] --- return results memorized
else
local newcolor = {red = r, green = g, blue = b}
results[key] = newcolor --- memorize new result
return newcolor
end
end
许多语言提供了组织其全局命名空间的机制,例如Modula中的模块、Java和Perl中的包或C++中的命名空间namespaces。这些机制都提供了一种基本机制来避免不同库中定义的名称之间的冲突。每个库都创建自己的命名空间,在该命名空间中定义的名称不会与其他名称空间中的名称发生冲突。
Lua中没有类似的机制,但是可以通过table去表示包。这种好处是可以像操作table一样来操作包,并用来创建额外的功能。
定义包的方法之一是将包名作为包中每个对象的前缀。例如,避免过多的显式包名,可以将一个局部变量赋值给包:
local P = {} --- a library to manipulate complex numbers
complex = P --- package name
function P.new (r, i) return {r=r, i=i} end
------
c1 = complex.new(10, 20) --- use the complex lib
包的私有性可以通过将函数定义为局部变量解决,同时为了避免访问权限变更时大规模修改代码,可以将所有的函数声明为局部函数,最后统一放入一个table中,同时还可以调用包内函数时避免显式包名。例如,为包加一个有合法性检验的add方法:
local function new (r, i) return {r=r, i=i} end
local function checkComplex (c) --- private function
if not ((type(c) == "table") and tonumber(c.r) and tonumber(c.i)) then
error("bad complex number", 3)
end
end
function add (c1, c2)
checkComplex(c1); checkComplex(c2);
return P.new(c1.r + c2.r, c1.i + c2.i)
end
complex = {new = new, add = add} --- final table(lib) to be exported
通常会将定义包的代码放在单个文件中,再通过require打开/导入包,例如require "complex.lua"(不能多次加载同一个包)。require是作用于文件的,因此用包来命名文件,或者增加一些后缀,但是后缀需要与路径匹配(例如,在/lualibs/?.lua路径中可能找到complex.lua的库)。Lua中,可以用文件名动态命名包,据此当一个包有多个文件时,只需要重命名文件即可。在包文件中使用_REQUIREDNAME变量,将在require时用虚拟文件名定义该变量,包会使用虚拟文件名注册。
“_G”是保存全局环境的全局变量(不是函数)。Lua本身不使用此变量,改变其值不会影响任何环境。在Lua中,任何对自由命名var的引用都会被语法转化为_ENV.var,代码块也会被名为_ENV的外部局部变量中编译。所有被用于_ENV的值的表被称为环境。Lua拥有一个称为全局环境的独特环境,并用其初始化"_G"。
如果用户将包放在文件cpx.lua中并运行require“cpx”,则包将加载到表cpx中。如果另一个用户将包移动到文件cpx_v1.lua并运行require“cpx_v1”,则该程序包将加载到表cpx_v1中。
local P = {} --- package
if _REQUIREDNAME == nil then
complex = P
else
_G[_REQUIREDNAME] = P
end
元方法可以为包创建一个专用的运行环境。如果改变包的主块的环境,它创建的所有函数都将共享这个新环境。
Lua是一种嵌入式语言,即Lua不是一个独立的软件包,而是一个可以与其他应用程序链接的库,以便将Lua功能集成到这些应用程序中。Lua能够独立运行的在于Lua解释器,这个解释器是一个小型应用程序(只有不到500行代码),它使用Lua库来实现独立的解释器。这个程序处理与用户的接口,将文件和字符串输入Lua库。这种被当作库来扩展应用程序的能力使Lua成为一种扩展语言。同时,使用Lua的程序可以在Lua环境中注册新函数;这些函数是用C(或另一种语言)实现的,并且可以添加不能直接用Lua编写的功能。
Lua的这两种视角(作为扩展语言和可扩展语言)对应于C和Lua之间的两种交互。在第一类中,C具有控件,Lua是库。这种交互中的C代码就是我们所说的应用程序。在第二类中,Lua具有控件,C是库。这里的C代码称为库代码。应用程序和库都使用相同的API与Lua通信,即所谓的C API。
C API是一组允许C代码与Lua交互的函数。它包括读写Lua全局变量、调用Lua函数、运行Lua代码片段、注册C函数以便以后可以被Lua代码调用等功能。(函数实际上是指函数或宏,API将几个功能作为宏实现)
C API遵循C的操作方式,这与Lua截然不同。当用C语言编程时,我们必须关注类型检查(和类型错误)、错误恢复、内存分配错误以及其他一些复杂的来源。API中的大多数函数不检查其参数的正确性;在调用函数之前,必须确保参数有效。如果出错的话,只会得到一个片段错误segmentation fault或其他错误,而不是一个完整的错误提示。一个任务可以调用多个C API,允许进行一些细节上的操作,例如错误处理、缓存尺寸等等。
Lua和C之间通信的主要组件是无所不在的虚拟堆栈。几乎所有API调用都对该堆栈上的值进行操作。从Lua到C以及从C到Lua的所有数据交换都通过该堆栈进行,还可以利用堆栈保存中间量。堆栈有助于解决Lua和C之间的冲突:第一个是Lua的垃圾回收与C的显式释放之间的冲突;第二个是Lua的动态类型与C的静态类型之间的冲突。
#include
#include
#include
#include
#include
int main (void) {
char buff[256];
int error;
lua_State *L = lua_open(); /* opens Lua */
luaopen_base(L); /* opens the basic library */
luaopen_table(L); /* opens the table library */
luaopen_io(L); /* opens the I/O library */
luaopen_string(L); /* opens the string lib. */
luaopen_math(L); /* opens the math lib. */
while (fgets(buff, sizeof(buff), stdin) != NULL) {
error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop(L, 1); /* pop error message from the stack */
}
}
lua_close(L);
return 0;
}
头文件lua.h定义了Lua提供的基本函数,包括创建基本Lua环境、调用Lua函数、读写Lua环境中的全局变量、注册Lua调用的新函数。每个lua.h中定义的有一个lua_前缀。
头文件lauxlib.h定义了辅助库中的函数,每个都有luaL_前缀。辅助库使用lua.h提供的基本API来提供更高的抽象级别。所有Lua标准库都使用auxlib,但auxlib无法访问Lua的内部,只能通过官方的基本API工作。
Lua库中没有定义全局变量。所有状态都保存在动态结构lua_state中,指向该结构的指针作为参数传递给lua中的所有函数。这种实现使Lua可重入,并可以在多线程代码中使用。
在C中任何建表函数(例如a[k] = v)都需要一个固定类型,因此单个操作需要多个函数。解决方法之一是在C中声明一个统一类型(例如void lua_settable (lua_Value a, lua_Value k, lua_Value v);),但有两个缺点:① 无法将一个复杂的类型映射到其他语言(例如C/C++、Java等);② 如果在C变量中保存Lua值,Lua的垃圾回收可能无法正常工作。因此Lua API使用了一个抽象的堆栈来交换数据,而不是定义一个lua_Value这样的类型。
当调用Lua请求其中的值时,Lua将需要的值推送到堆栈。当向Lua传递值时,需要用不同的函数推送不同的C类型以及一个函数来从堆栈接收值。堆栈是由Lua管理的,垃圾回收可以正常工作。Lua管理的堆栈严格执行LIFO规则。但C代码对堆栈更自由,可以遍历以及插入、删除任意位置的元素。
C API中对每个Lua类型有一个推送函数,用C表示为lua_pushnil、lua_pushnumber、lua_pushboolean、lua_pushlstring和lua_pushstring。
void lua_pushnil (lua_State *L); /* constant nil */
void lua_pushboolean (lua_State *L, int bool); /* integers 0 1 */
void lua_pushnumber (lua_State *L, double n); /* double */
void lua_pushlstring (lua_State *L, const char *s, size_t length); /* char*任意字符串 */
void lua_pushstring (lua_State *L, const char *s); /* 0结尾的传统字符串 */
Lua中的字符串是非0结尾的,依赖于显式长度,可以包含任意的二进制数据。官方函数lua_pushlstring需要一个显式长度作为参数。lua_pushstring用于0结尾字符串,使用strlen获取字符串长度。对于必须保留的任何字符串,Lua要么创建一个内部副本,要么重用一个。因此,只要这些函数返回,就可以释放或修改缓冲区。
当推送元素到堆栈时,必须确保有空间。当Lua启动并且Lua调用C时,堆栈至少有20个空闲槽(在Lua.h中定义为Lua_MINSTACK)。可以通过调用int lua_checkstack (lua_State *L, int sz); 检查堆栈的空间。
C API使用索引来引用堆栈中的元素,堆栈中第一个元素(堆底,第一个推送的元素)的索引为1,堆栈中最后一个元素(堆顶,最后一个推送的元素)的索引可以用-1表示。例如,调用lua_tostring(L, -1),将堆顶元素返回为string。
C API提供了一系列函数检查元素的类型,int lua_is*(lua_State *L, int index),*可以是任意Lua中的类型,例如,lua_isnumber、lua_isstring、lua_istable等。其中,lua_isnumber和lua_isstring检查该值能否被转化为Lua类型。某些lua_is*()函数本质上是使用lua_type函数的宏。lua_type()函数返回堆栈中元素的类型,每个类型都在头文件lua.h中用常量定义:LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA,和LUA_TTHREAD。通常和switch一起使用。
C API提供了一系列函数获取堆栈中的值,ctype lua_to*(lua_State *L, int index)。
int lua_toboolean (lua_State *L, int index);
double lua_tonumber (lua_State *L, int index);
const char *lua_tostring (lua_State *L, int index);
size_t lua_strlen (lua_State *L, int index);
当元素的类型不正确时,也可以调用,此时根据调用的函数的返回值类型返回0或NULL。lua_tostring函数返回指向字符串内部副本的指针,Lua确保只要相应的值在堆栈中,该指针就有效。当C函数返回时,Lua清除其堆栈;因此,不能在获取Lua字符串的函数之外存储指向Lua字符串。
int lua_gettop (lua_State *L); |
返回堆栈中的元素数,即顶部元素的索引。 |
void lua_settop (lua_State *L, int index); |
将堆顶设为给定的值。若原堆比新堆高,则删除多余元素;反之,推送nil使堆达到指定的高度。例如lua_settop(L, 0),相当于清空堆。例如宏#define lua_pop(L,n) lua_settop(L, -(n)-1),相当于删除堆顶n个元素。 |
void lua_pushvalue (lua_State *L, int index); |
将给定索引处的元素的副本推送到堆顶。 |
void lua_remove (lua_State *L, int index); |
删除给定索引处的元素,向下移动该位置以上的所有元素以填充空槽。 |
void lua_insert (lua_State *L, int index); |
将顶部元素移动到给定索引处,将该位置以上的所有元素上移到开放空间。 |
void lua_replace (lua_State *L, int index); |
删除堆顶的元素,并将其值设为给定索引处的值。 |
以下是调用C API的例子,用于遍历堆栈并根据元素类型打印元素:
#include
#include
static void stackDump (lua_State *L) {
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; i++) { /* repeat for each level */
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING: /* strings */
printf("`%s'", lua_tostring(L, i));
break;
case LUA_TBOOLEAN: /* booleans */
printf(lua_toboolean(L, i) ? "true" : "false");
break;
case LUA_TNUMBER: /* numbers */
printf("%g", lua_tonumber(L, i));
break;
default: /* other values */
printf("%s", lua_typename(L, t));
break;
}
printf(" "); /* put a separator */
}
printf("\n"); /* end the listing */
}
int main (void) {
lua_State *L = lua_open();
lua_pushboolean(L, 1); lua_pushnumber(L, 10);lua_pushnil(L); lua_pushstring(L, "hello");
stackDump(L); /* true 10 nil `hello' */
lua_pushvalue(L, -4);
stackDump(L); /* true 10 nil `hello' true */
lua_remove(L, -3);
stackDump(L); /* true 10 `hello' true */
lua_close(L);
return 0;
}
扩展Lua的基本方法之一是应用程序将新的C函数注册到Lua中。
对于从Lua调用的C函数,必须遵循协议来获取其参数并返回其结果。此外,对于从Lua调用的C函数,必须注册,即必须以适当的方式将其地址赋予Lua。
当Lua调用C函数时,它使用与C调用Lua相同的堆栈。C函数从堆栈中获取其参数,并将结果推送到堆栈中。堆栈不是全局结构,每个函数都有自己的私有局部堆栈。当Lua调用C函数时,第一个参数将始终位于局部堆栈的索引1。即使当一个C函数再次调用相同(或另一个)C函数的Lua代码时,这些调用中的每一个都只能看到自己的局部堆栈。