词法规范:
Lua执行的每段代码,例如一个源代码文件或在交互模式中输入的一行代码,都称为一个“程序块”。在lua的语法中,代码中的换行不起任何作用。
Lua – i xxx 调试和手工测试
不要用下画划线跟着大写字母的标识符,lua将这类标识用于特殊用途。
Lua是有大小写之分的。
--注释
--[[
Xxxxxx
--]]
全局变量:
全局变量没有初始化时,值是nil。
如果要删除一个全局变量,赋值nil就行了。
类型与值
在lua中有8种基础类型:nil(空)、boolean(布尔)、number(数字)、string(字符串)、userdata(自定义类型)、function(函数)、thread(线程)和table(表)。函数type可根据一个值返回其类型名称。
Number类型用于表示实数。
Table(表):
Table类型实现了“关联数组(associative array)”。“关联数组”是一种具有特殊索引方式的数组。
Table既不是“值”也不是“变量”,而是“对象”。
虽然可以用任何值作为一个table的索引,也可以用任何数字作为数组索引的起始值。但就lua的习惯而言,数组通常以1作为索引的起始值。
长度操作符“#”用于返回一个数组或线性表的最后一个索引值(或为其大小)。Lua将nil作为界定数组结尾的标志。
Lua操作符的优先级:
^
not #(长度操作符) -(一元负号)
* / %
+ -
..(字符串连接)
< > <= >= ~= ==
and
or
赋值
Lua允许“多重赋值”,也就是一下子将多个值赋予多个变量。每个值或每个变量之间以逗号分隔。例如:
a,b = 10,2*x
Lua总是会将等号右边值的个数调整到与左边变量的个数相一致。规则是:若值的个数少于变量的个数,那么多余的变量会被赋为nil;若值的个数更多的话,那么多余的值会被“静悄悄地”丢弃掉;
与全局变量不同的是,局部变量的作用域仅限于声明它们的那个块。一个块(block)是一个控制结构的执行体、或者是一个函数的执行体再或者是一个程序块(chunk);
控制结构
Lua将所有不是false和nil的值视为“真”。Lua将0和空字符也视为真。
Lua不支持switch语句,所以这种一连串的if-else if代码是很常见的。
For语句有两种形式,数字型for(numeric for)和泛型for(generic for)。
泛型for循环通过一个迭代器函数ipairs()来遍历所有值。
函数
在lua中,函数是一种对语句和表达式进行抽象的主要机制。
若实参多余形参,则舍弃多余的实参;
Lua允许函数返回多个结果:
在多重赋值中,若一个函数调用是最后的(或仅有的)一个表达式,那么Lua会保留其尽可能多的返回值,用于匹配赋值变量。
x,y = foo2() --x="a",y="b"
x = foo2() --x="a","b"被丢弃
x,y,z = 10,foo2() --x=10,y="a",z="b"
如果一个函数没有返回值或者没有返回足够多的返回值,那么lua会用nil来补充缺失的值。
x,y = foo0() --x=nil,y=nil
x,y = foo1() --x="a",y=nil
x,y,z = foo2() --x="a",y="b",z=nil
关于多重返回值还要介绍一个特殊函数——unpack。它接受一个数组作为参数,并从下标1开始返回该数组的所有元素:
print(unpack{10,20,30}) -->10 20 30
a,b = unpack{10,20,30} --a=10,b=20,30被丢弃
unpack的一项重要用途体现在“泛型调用(generic call)”机制中。泛型调用机制可以动态地以任何实参来调用任何函数。举例来说,在ANSIC C中是没有办法编写泛型调用的代码。最多是声明一个能接收变长参数的函数(通过stdarg.h),或者使用一个函数指针来调用不同的函数。并且在C语言中,无法在同一次函数调用中传入动态数量的参数。也就是说,在每次调用函数时必须传入固定数量的参数,并且每个参数都具有确定的类型。然而在lua中就可以做到这点。如果想调用任意函数f,而所有的参数都在数组a中,那么可以这么写:
f(unpack(a))
Lua中的函数还可以接受不同数量的实参。参数表中的3个点(...)表示该函数可接受不同数量的实参。
Lua提供专门用于格式化文本(string.format)和输出文本(io.write)的函数。
深入函数
在lua中有一个容易混淆的概念是,函数与所有其他值一样都是匿名的,即它们都没有名称。当讨论一个函数名时(例如print),实际上是在讨论一个持有某函数的变量。这与其他变量持有各种值一个道理,可以以多种方式来操作这些变量。
Lua中的函数还有一个有趣的特征,那就是lua支持“尾调用消除(tail-call elimination)”。
所谓“尾调用(tail call)”就是一种类似于goto的函数调用。当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”。
迭代器与泛型for
所谓“迭代器”就是一种可以遍历(iterate over)一种集合中所有元素的机制。
对于迭代器而言,一种常见的情况就是:编写迭代器本身或许不太容易,但使用它们却是很容易的。
尽可能地尝试编写无状态的迭代器,那些迭代器将所有状态保存在for变量中,不需要在开始一个循环时创建任何新的对象。如果迭代器无法套用这个模型,那么就应该尝试使用closure。closure显得更加优雅一点,通常一个基于closure实现的迭代器会比一个使用table的迭代器更为高效。
“迭代器”这个名称多少有点误导的成分。因为迭代器并没有做实际的迭代,真正做迭代的是for循环。而迭代器只是为每次迭代提供一些成功后的返回值。或许,更准确地应称其为“生成器(generator)”。不过“迭代器”这个名称已在其他语言中被广泛使用,例如Java。
编译、执行与错误
尽管将Lua称为是一种解释型的语言,但Lua确实允许在运行源代码前,先将源代码预编译为一种中间形式。听上去“编译”似乎不应在一种解释型语言的范畴之列。其实,区别解释型语言的主要特征并不在于是否能编译它们,而是在于编译器是否是语言运行时库的一部分,即是否有能力(并且轻易地)执行动态生成的代码。可以说正是因为存在了诸如dofile这样的函数,才可以将Lua称为是一种解释型的语言。
编译
前面已经提到了dofile函数,它是一种内置的操作,用于运行Lua代码块。但实际上dofile是一个辅助函数,loadfile才做了真正核心的工作。类似于dofile,loadfile会从一个文件加载Lua代码块,但它不会运行代码,只是编译代码,然后将编译结果作为一个函数返回。此外,与dofile不同的还有loadfile不会引发错误,它只是返回错误值并不处理错误。
对于简单任务而言,dofile非常便捷,它在一次调用中做完了事件事情。然而loadfile更灵活,在发生错误的情况中,loadfile会返回nil及错误消息,这便可以按自定义的方式来处理错误。此外,如果主可以了。相对于多次调用dofile来说,由于只编译一次文件,开销就小得多了。
函数loadstring与loadfile类似,不同之处在于它是从一个字符串中读取代码,而非从文件读取。loadstring的功能是非常强大的,但应该谨慎使用。
Lua将所有独立的程序块视为一个匿名函数的函数体,并且该匿名函数还具有可变长实参(variable number of arguments)。
c代码
与Lua代码不同的是,C代码需要在使用前先链接入一个应用程度。在大多数主流系统中,达成这种链接最简单的方法是动态链接机制。不过,动态链接却不是ANSI C标准的一部分,也就是说不存在任何可移植的方案来实现它。
Lua通常不会包含任何无法通过ANSI C来实现的机制。不过,动态链接却有些不同。将其视为所有其他机制的母机制,只要拥有它,就可以动态地加载任何其他不在Lua中的机制了。因此,对于这项特殊情况Lua打破了其对于可移植性的准则,为几种平台实现了一套动态链接机制。
协同程序(coroutine)
协同程序与线程(thread)差不多,也就是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协同程序共享全局变量和其他大部分东西。从概念上讲线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。就是说,一个具有多个协同程序的程序在任意时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显式地要求挂起(suspend)时,它的执行才会暂停。
协同程序基础
Lua将所有关于协同程序的函数放置在一个名为“coroutine”的table中。
数据结构
11.1数组
使用整数来索引table即可在Lua中实现数组。因此,数组没有一个固定的大小,可以根据需要增长。通常,当初始化一个数组时,也就间接地定义了它的大小。
然而,在Lua中的习惯一般是以1作为数组的起始索引。Lua库和长度操作符都遵循这个约定。如果你的数组不是从1开始的,那就无法使用这些功能了。
11.2矩阵与多维数组
在Lua中,有两种方式来表示矩阵。第一种是使用一个“数组的数组”,也就是说,一个table中的每个元素是另一个table。
第二种方式是将两个索引合并为一个索引。如果两个索引是整数,可以将第一个索引乘以一个适当的常量,并加上第二个索引。
如果索引是字符串,那么可以把索引拼接起来,中间使用一个字符来分隔。
通常应用程序会用到一种特殊的矩阵,称为“稀疏矩阵”,这种矩阵中的大多数元素为0或nil。可以通过稀疏矩阵来表示一个图(graph)。
11.3 链表
由于table是动态的实体,所以在Lua中实现链表是很方便的。每个结点以一个table来表示,一个“链接”只是结点table中的一个字段,该字段包含了对其他table的引用。其中每个结点具有两个字段:next和value。
11.4 队列与双向队列
在Lua中实现队列的一种简单方法是使用table库的函数insert和remove。
元表(metatable)与元方法(meatmethod)
可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。
Lua中的每个值都有一个元表。table和userdata可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。Lua在创建新的table时不会创建元表。
可以使用setmetatable来设置或修改任何table的元表。
任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表描述了它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。总之,任何搭配形式都是合法的。
在Lua代码中,只能设置table的元表。若要设置其他类型的值的元表,则必须通过C代码来完成。
库定义的元方法
各种程序库在元表中定义它们自己的字段是很普遍的方法。到目前为止介绍的所有元方法都只针对于Lua的核心,也就是一个虚拟机(virtual machine)。它会检测一个操作中的值是否有元表,这些元表中是否定义了关于些操作的元方法。从另一方面说,由于元表也是一种常规的table,所以任何人、任何函数都可以使用它们。
环境
Lua将其所有的全局变量保存在一个常规的table中,这个table称为“环境(environment)”。
这种组织结构的优点在于,其一,不需要再为全局变量创造一种新的数据结构,因此简化了Lua的内部实现。另一个优点是,可以像其他table一样操作这个table。为了便于实施这种操作,Lua将环境table自身保存在一个全局变量_G中。
全局变量声明
Lua中的全局变量不需要声明就可以使用。对于小型程序来说较为方便,但在大型程序中,一处简单的笔误就有可能造成难以发现的错误。不过这种性能可以改变。由于Lua将全局变量存放在一个普通的table中,则可以通过元表来改变其访问全局变量时的行为。
模块与包
模块系统的一个主要目标是允许以不同的形式来共享代码。包就是一系列模块。
编写模块的基本方法
在Lua中创建一个模块最简单的方法是:创建一个table,并将所有需要导出的函数放入其中,最后返回这个table。
面向对象编程
Lua中的table就是一种对象,这句话可以从3个方面来证实。首先,table与对象一样可以拥有状态。其次,table也与对象一样拥有一个独立于其值的标识(一个self)。例如,两个具有相同值的对象(table)是两个不同的对象。最后,table与对象一样具有独立于创建都和创建地的生命周期。
CAPI概述
Lua是一种嵌入式语言。即lua不是一个单独运行的程序,而是一个可以链接到其他程序的库。通过链接就可以将Lua的功能合并入这些程序。
这种使用一个库来扩展应用程序的能力使得Lua成为一种“扩展语言(Extension Language)”。而与此同时,一个使用了Lua的程序可以在Lua环境中注册用C语言(或其他语言)实现的新函数,由此就可以向Lua添加某些无法直接用Lua编写的功能,这便使Lua成为一种“可扩展的语言(extensible language)”。
C API是一组能使C代码与Lua交互的函数。其中包括读写Lua全局变量、调用Lua函数、运行一段Lua代码,以及注册C函数以供Lua代码调用等。
Lua和c语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈来完成。栈可以解决Lua和C语言之间存在的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显式地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型。
头文件lua.h定义了lua提供的基础函数,包括创建lua环境、调用lua函数(如lua_pcall)、读写lua环境中全局变量,以及注册供lua调用的新函数等。Lua.h中定义所有内容都有一个lua_前缀。
头文件lauxlib.h定义了辅助库(auxiliary library,auxlib)提供的函数。它的所有定义都以luaL_开头(如luaL_loadbuffer)。辅助库是一个使用lua.h中API编写出的一个较高的抽象层。Lua的所有标准库编写都用到了辅助库。
Lua库中没有定义任何全局变量,它将所有的状态都保存在动态结构lua_State中。
Lua严格地按LIFO(Last in,First out,先出后进)规范来操作这个栈。当调用Lua时,Lua只会改变栈的顶部。不过,C代码则有更大的自由度,它可以检索栈中间的元素,甚至在栈的任意位置插入或删除元素。
24.2.1压入元素
向栈中压入一个元素时,应该确保栈中具有足够的空间。当Lua启动时,或Lua调用C语言时,栈中至少会有20个空闲的槽。要检查栈中是否有足够的空间,可以调用lua_checkstack。
24.2.2 查询元素
API使用“索引”来引用栈中的元素。第一个压入栈中的元素索引为1;还可以以栈顶为参考物,使用负数的索引来访问栈中的元素。此时-1表示栈顶元素(最后压入的元素)。
为了检查一个元素是否为特定的类型,API提供了一系列的函数lua_is*,其中*可以是任意Lua类型。
当Lua调用的一个C函数返回时,Lua就会清空它的栈。这就形成了一条规则,不要在C函数之外使用在C函数内获得的指向Lua字符串的指针。
24.2.3 其他栈操作
lua_gettop函数返回栈中元素的个数,也可以说是栈顶元素的索引。lua_settop将栈顶设置为一个指定的位置,即修改栈中元素的数量。如果之前的栈顶比新设置的更高,那么高出来的这些元素会被丢弃;反之,会向栈中压入nil来补足大小。lua_settop(L,0)能清空栈。
24.3 C API中的错误处理
C语言不同于C++和Java,它没有提供异常处理机制。Lua中所有的结构都是动态的,它们会根据需要来增长,或者缩小。
24.3.2 库代码中的错误处理
lua是一种安全的语言,无论写什么,写出来的内容是否正确,都能用Lua自身的术语来理解程序的行为。此外,错误也是通过Lua的术语来检测和解释的。可以用C语言来作一个对比,许多C程序的错误行为只能用底层硬件的术语来解释,而错误位置则是由“程序计数器”寄存器给出的。
扩展应用程序
lua的一项重要用途就是作为一种配置语言(configuration language)。
lua_getglobal会将相应的全局变量值压入栈中。