本章节主要讲Lua这门语言的基本概念
1.基本概念
1.1 值与类型
Lua是一门动态类型的语言,也就是说声明一个变量是没有类型的,它取决于被所赋值的类型,所有的值都有属于它自己的类型。
Lua的所以值都是一级值,也就是可以被存储到一个变量中,可当做参数进行传输也可以当做结果被返回。
Lua里包含了8种基本类型:nil, boolean, number, string, function, userdata, thread和table。nil这个特别的值,意味着它跟其他所有值都不同表示是没有被赋值过任何值。boolean就只有true或false这两种值。只有false和nil在条件语句中表示为非真,其他任何值都表示为真。number表示整型或浮点型数值。string表示为不可变的字符。Lua每个字节都是8位的,字符串可以包括任何8位的字节,包括空字节(‘\0’)。Lua是与编码无关的,因此就字符串的内容不会进行臆断编码。
number包含两种简单类型,一种叫整型另一种叫浮点型。Lua在使用这两种类型时将会自动进行转换(见:2.4.3)。因此程序员绝大部分可以忽略整型与浮点型之间的差别或者对数字表示全部控制。通常Lua采用64位整型和双精度浮点,当然你也可以重新编译Lua采用32位整型和单精度浮点。尤其是在小型机器和嵌入系统中都是采用32位来表示整型和浮点型(查看luaconf.h中的LUA_32BITS宏定义)。
Lua可以调用(操纵)Lua或C写的函数功能(见:2.4.10)。这些都是属于function类型。
userdata是允许任意C数据存储到Lua变量里的类型,一个userdata就表示原始的内存块,它有两种类型:full userdata是为由Lua管理的一块内存块对象,light userdata为C的简单指针值。userdata在Lua中除了分配和验证以外就没有定义别的操作。通过使用metatables开发者就可以为full user的值定义操作(见:1.4)。userdata的值只能通过C的API来创建和修改,不能通过Lua来创建和修改,这是为了在宿主程序中保证数据的完整性。
thread是用于描述创建协程的类型表示可执行的独立线程(见:1.6)。Lua的线程跟操作系统的线程半毛钱关系都没有。Lua在无论是否支持多线程的所有系统中都支持协同线程。
table类型可以说是关联数组,它的索引不仅可以采用数字来表示外还可以采用除了nil和NaN(Not a Number是一种不确定或无法表示的特殊数字值,例如:0/0)以外的任何Lua值来表示。关系表是可以异构,也就是说它可以包含任何类型的值(nil除外),当值为nil也就表示这key不存在于关系表上。相反而言,任何没有在关系表上的key都会关联一个为nil的值。
关联表在Lua是唯一有数据结构的机制的类型,可以用来表示普通数组,序列,符号表,集,记录,图,树数据结构等。就记录而言,Lua可以用字段名当做索引。可以采用a.name或a[“name”]来获取关联表中一个key的值,Lua中有几种创建表的方法(见:2.4.9)。
我们使用有限序列时在关联表是可以用{1..n}来表示的,n表示序列的长度,其中所有的key都是为正整数(见:2.4.7)。
关联表字段里的值也是可以包含所以类型,也就是说表字段里也可以包含函数。因此表也可以具有方法的功能(见:2.4.11)。
关系表的索引遵循自然语言的等式定义,a[i]和a[j]这个表达式表示仅i和j相等时才是同一个表里的元素(这里的相等不是元方法),特别是一个整数浮点是等于它自身的整型(例如:1.0==1)。为了避免含糊不清,任何整数浮点当做表的key时将自动转换为整型数据。例如:a[2.0] = true,实际在表中的key的值为2而不是2.0(也就是说2跟”2″是不同的,他们在表中表示不同的项)。
table,function,thread和 (full) userdata这些类型的值都是对象:变量只是引用它们,而非实质的包含。赋值,参数传送和函数的返回值都是对这些值的引用而并非对其进行复制。
在库类中的type函数就是来获取上面各种类型的字符串值(见:5.1)。
1.2 环境和全局环境
引用任何一个没有声明的var将会自动改用为_ENV.var,关于这个将会在 2.2 和 2.3.3这两小节上进行阐述。此外每块代码都在包含有_ENV这个本地变量的执行域内(见:2.3.2),所以_ENV在代码中是已经声明了的。
尽管存在着_ENV这样的外部变量和对未声明的名称进行转换,但是它也是个正规的名字。特别是对_ENV进行重新定义或当参数传送,每次引用未声明的名字时将会优先使用可执行域上的_ENV,都是遵循Lua的执行域规则(见:2.5)
任何表被当做_ENV的值时被称为环境。
Lua把一个特别的环境称做全局环境,它的值保存在C注册表的一个特殊关键字上(见:3.5)。在Lua中采用_G用来表示这个全局环境(_G从不在内部使用)。
当Lua加载一块代码时,_ENV的默认值将变为全局环境(见:load)。因此在默认情况下所有的未声明名称将会指向到全局环境的项(因此它们也被叫做全局变量)。此外所以的标准库类被加载到全局环境后并会在该环境下运行某些函数。可以调用load(或者loadfile)去加载一块不同环境下的代码(在C语言中,必须加载代码然后在第一赋值时进行改变它们的值)。
1.3 错误处理
因为Lua是一门嵌入式扩展语言,所以在C写的宿主程序中所有的Lua操作都是从调用Lua库类的一个函数开始(当只运行Lua脚本时,lua应用程序就是宿主程序)。每当Lua代码块出现运行时错误时,将交由宿主程序去采取些适当的措施进行处理(例如打印错误信息等)。
Lua可以显式的调用error这个函数去产生一个错误信息,如果想捕获错误信息,可以使用保护模式下的pcall或xpcall去调用一个函数。
每当一个带有信息的错误,错误对象(也称为错误信息)被传播,都是直到由Lua程序或宿主处理这个错误对象为止。Lua产生的错误对象都是字符串形式的,而程序则可以产生各种类型值的错误对象。
当使用xpcall或lua_pcall时,可以给定一个错误信息处理函数。这个函数被调用时会传入一个错误信息并返回一个新的错误而且是在展开错误栈之前被调用的,这样就可以通过检查堆栈并创建一个堆栈的追溯,来获取这个错误的更多信息。这个消息处理程序仍然是在保护的情况下继续调用;因此当消息处理程序内部的错误将再次调用消息处理程序进行处理,如果循环太久了,Lua将自动退出并返回一个适当的信息。
1.4 元表和元方法
在Lua中每种值都可以叫元表。所谓的元表就是定义了对原始值可以进行某些操作行为的普通的Lua表。你可以通过设定元表某些特定字段的值,从而实现特有方面的操作行为。例如当进行一个非数字的加法操作时,Lua会检查这个值的元表中”__add”字段的一个函数,如果存在则调用这个函数来执行加法操作。
这些在元表的关键字段都是衍生自事件名字:它们对应的值被叫做元方法。在上面的例子中是”add”事件而元方法则是所执行加法的函数。
你可以调用getmetatable函数来查询元表里的任何值。
你可以调用setmetatable来改变表里的元表。你只能用C语言的API来改变,而在Lua里是无法改变其它类型的元表(除非使用debug库(见:5.10))。
表和full userdata有特别的元表(虽然多个表和userdata可以共享它们的元表)。其他类型的值都为各自享有一个单独的元表;也就是数字类型拥有一个元表,字符串类型也是拥有一个元表,其他的也一样。默认情况下,一个单独的值是没有元表的,而字符串库类为字符串设置了一个元表(见:5.4)
一个元表控制着一个对象如何进行算术计算,位操作,顺序比较,串联,长度操作,调用和索引,它也可以定义一个函数用于表或userdata进行回收时调用(见:1.5)。
在下面有详细的被元表控制的事件列表。每种操作都有与之匹配的事件名称。在元表中的字段就是事件名称前加两个下划线”__”;例如add事件的字段名则为”__add”。注意所有的元方都是直接获取的;也就是访问元方法不会再调用其它元方法。
元表调用一元操作符(求反,求长度,位反)时,第二个参数是哑元,而值等于第一个参数。之所以这样做是为了简化Lua的内部实现(这样是为了让所有操作跟二元操作一样)而且有可能会在以后的版本里被移除(使用额外参数都是无关紧要的)。
- “add”:+运算。如果对任何非数字的值(包括不能转换成数字的字符串)做加法,Lua就会尝试调用一个元方法。首先Lua会检查第一操作数(即使它是合法的),如果它没有为”__add”事件定义元方法,那么Lua就会检查第二个操作数,一旦找到这个元方法并调用它,Lua将把这两个操作数当作这个元方法参数,并将元方法执行的结果(调整为单值)当做这个操作的结果。如果没有找到则抛出一个错误。
- “sub”;-运算。跟”add”的行为一样。
- “mul”;*运算。跟”add”的行为一样。
- “div”;/运算。跟”add”的行为一样。
- “mod”;%运算。跟”add”的行为一样。
- “pow”;^(取幕)运算。跟”add”的行为一样。
- “unm”;-(取负)运算。跟”add”的行为一样。
- “idiv”;//(先下取整)运算。跟”add”的行为一样。
- “band”;&(位与)运算。跟”add”的行为一样。不同的是Lua在任何一个操作数不能转换成整型时,才会去调用元方法(见:2.4.3)。
- “bor”;|(位或)运算。跟”band”的行为一样。
- “bxor”;~(位异或)运算。跟”band”的行为一样。
- “bnot”;~(位非)运算。跟”band”的行为一样。
- “shl”;<<(左移)运算。跟”band”的行为一样。
- “shr”;>>(右移)运算。跟”band”的行为一样。
- “concat”;..(串联)运算。跟”add”的行为一样,不同的是Lua在任何一个不是字符串或数字(数字总是可以强制转换成字符串)操作数,才会去调用元方法。
- “len”;#(求长)运算。如果这个对象不是字符串,Lua将会尝试它的元方法。如果存在元方法则调用它将这个对象当作它的参数,并将元方法执行的结果(调整为单值)当做这个操作的结果。如果对象是一个表并不存在元方法时,则Lua将会调用表的取长操作(见:2.4.7)。其它情况,Lua则抛出一个错误。
- “eq”;==(等于)运算。跟”add”的行为一样,不同的是Lua在两个值都为表或full userdata且不为同一对象进行比较时才会调用元方法,调用的结果总是转换为boolean类型。
- “lt”;<(小于)运算。跟”add”的行为一样,不同的是Lua在两个值都不是数字或字符串进行比较时才会调用元方法,调用的结果总是转换为boolean类型。
- “le”;<=(小于等于)运算。它不同于其它的操作,小于等于可能会调用两个不同的事件。首先跟”lt”操作的行为一样,Lua会在两个操作数寻找”__le”元方法。如果都没有找到,则就会尝试查询”__lt”事件,就会假设为a<=b等价于not(b > a)。跟其它的比较操作一样,返回结果都是boolean类型。(在以后的版本里将有可能移除”__lt”元方法的调用;而是变成只调用”__le”元方法。)
- “index”;table[key]的索引访问。当table不是表类型或key不存在于表中就会触发这个事件。此时会读出表的元方法。
尽管取这样的名字,但这个事件的元方法可以是一个函数也可以是表。如果是一个函数,则以 table 和 key 作为参数调用它。如果是一张表,则会以key为索引来取这张表的值。(这个索引过程是走常规的流程,而不是直接索引, 所以这次索引有可能引发另一次元方法) - “newindex”;索引赋值table[key]=value。跟”index”的行为一样,当table不是表类型或key不存在于表中就会触发这个事件。因此就会调用表的元方法。
跟索引操作一样,这个事件的元方法也是可以是一个函数也可以是表。如果是一个函数,则以 table , key和value 作为参数调用它。如果是一张表,则会对这张表进行以key进行索引赋值。(这个索引过程是走常规的流程,而不是直接索引赋值, 所以这次索引赋值有可能引发另一次元方法)
一旦有了”newindex”元方法,Lua就不会走常规流程进行赋值。(如果有必要,在元方法内部可以调用 rawset 来做赋值。) - “call”;函数调用操作func(args)。当Lua调用一个非函数的值则会触发这个事件(也就是func不是一个函数)。查询func是否存在这个元方法。如果存在,就调用这个元方法,将func作为第一个参数,原来调用的参数(args)后依次排在后面。
有个很好的方式为一些对象设置元表时就是先将必须的元方法放到表后在设置。特别是在一个特殊命令到达时”__gc”元方法才会被调用(见:1.5.1)。
1.5 垃圾回收
Lua采用了内存自动管理。意味着你不用操心要为创建一个新对象怎么样分配内存或不再使用时如何释放内存。Lua运行着一个垃圾回收器来收集所有死对象(就是Lua不可能在访问到的)来完成自动内存管理工作。Lua里所以的内存使用,如:字符串,表,userdata,函数,线程,内部结构等都是服从自动管理。
Lua实现一个增量标记-扫描回收器。它使用两个数字来控制垃圾回收循环:垃圾回收器间歇和垃圾回收器倍率。这两个数字都使用百分数为单位(例如:100在内部表示为1)。
垃圾回收器间歇控制着回收器在开始新循环前要等多久。增大这个值会减少回收器的积极性。当这个值小于100则说明回收器无需等待就可以进行新的循环。这个值为200则说明会让回收器等到总内存用量达到之前的两倍时才进行新的循环。
垃圾回收器倍率控制着回收器运行速度相对于内存分配速度的倍率。增大这个值会增加回收器的积极性同时也增加了每个增量步骤的大小。不要将它设置小于100的值,不然回收器将工作的非常慢以至于可能永远都完成不了一次循环。默认是200,意味这回收器用两倍于内存分配的速度运行着。
如果你将进步倍率设置为一个非常大的值(比你程序用到的最大字节还大10%)就会变成一个stop-the-world回收器。如果将间歇设置为200,回收器的行为就跟Lua的老版本一样,每次Lua使用内存翻倍时,就会执行一次完整的收集。
你可以在C中调用lua_gc或在Lua中调用collectgarbage来改变这两个值。这两个函数也可以直接控制回收器(例如停止或重启)。
1.5.1 垃圾收集元方法
你可以为表设置垃圾回收元方法,用C语言API为full userdata设置垃圾回收元方法(见:1.4)。这些元方法也叫终结者。终结者允许你配合Lua垃圾回收器对其它额外资源进行管理(类似关闭文件,网络或数据库连接,或释放内存)。
当对一个对象(表或userdata)在回收过程中进行终结,那么就必须标记它触发终结器。当你对一个对象设置元表并元表有一个”__gc”的字段时,就标记这个对象可以触发回收器。注:当你为一个对象设置没有带”__gc”字段的元表或后面在对这个元表添加该字段,这样是不会标记触发终结器的。
当一个被标记的对象变成垃圾时,垃圾回收器并不会立刻进行回收。相反的是Lua会将它放入一个列表中。在回收过后,Lua会遍历这个列表。Lua会检查这个列表里的每个对象是否有__gc这个元方法:如果它是个函数,那么就把对象当成唯一参数调用它,否则忽略过去。
在每个垃圾回收循环的最后阶段,本次循环中检测到需要被回收的对象,其终结器的触发顺序按给对象触发标记的倒序进行;也就是说,第一被终结者调用的是在程序中最后一个被标记的对象。在执行常规代码的任何时刻都有可能执行终结器。
因为被回收的对象还要被终结器使用,所以对象(和通过它访问到的其它对象)必须被Lua复活。通常,复活是短暂的,将在下一个垃圾回收循环中释放其内存。然而,如果终结器贮存一些全局对象(例如:全局变量),那么这次复活就持续生效了。此外如果终结器一个进入终结流程的对象再做一次触发标记,当这个对象下一次循环中依旧不可获取将会在次调用它的终结函数。无论那种情况,不能取得的和没有被标记进入终结过程的对象,它们都只能在一个GC循环中被释放内存。
当你关闭一个状态机(见:lua_close)的时候,Lua将按照被标志对象的倒序进行调用它的终结过程。在这个过程中,任何终结器再次对对象进行标记是无效的。
1.5.2 弱表
所谓的弱表就是一个所有元素都是弱引用的表。垃圾回收器会忽略一个弱引用对象。对其他而言,如果一个对象的引用是弱引用的话,垃圾回收器将会回收它。
一个弱表可以有弱关键字,弱值或者两者都有。带弱值的表是可以回收它的值的,但是不能回收他的键值。由弱键和弱值组成表是可以回收。无论那种情况,如果键或值被回收时,则这对键值会在表中移除掉。判断表是否为弱表是由它的__mode元方法所控制的。如果__mode元方法是一个包含字符’k’的字符串,就表示表的键为弱键,如果包含’v’,则表示表的值为弱值。
一个由弱键强值组成的表也叫蜉蝣表。在一个蜉蝣表中,值是否可以获取取决于它的键是否可获取。特别注意的是,如果一个键仅仅通过它的值所引用,那么这对键值在表中将被移除。
对一张表弱属性进行修改将会在下一轮回收循环才生效。特别是把一张表的弱属性改为强属性时,Lua有可能在修改生效前回收表中的一些项。
只有那些有显式构造过程的对象才会被弱表移除。例如像数字和C函数的值,就不是垃圾回收的对象,因此它们也不会被表所移除(除非它们的关联值被回收了)。尽管字符串是垃圾回收的对象,但是它没有显式构造过程,所以它不会从弱表中移除。
弱表针对复活的对象(指那些正在走终结流程对象和可被正在走终结流程对象访问的对象)是一个特殊的行为。弱值引用的对象在运行它们的终结器之前被移除,而弱键键在运行完终结器后,在下轮回收时对象真正被释放了才被移除。这个行为允许终结器来访问该对象在弱表中相关联的属性。
如果一个弱表是在回收循环中的复活对象,那么在下一轮回收循环前这张表都有可能没有被正确清理。
1.6 协程
Lua支持协程,也称为协同多线程。在Lua中协程代表着一个可独立执行的线程。然而与多线程系统的线程不同的是,协程仅显式调用一个让出(yield)函数时挂起当前的执行。
调用coroutine.create函数来创建一个协程。它的唯一参数也就是其主函数。create函数只是创建一个协程并返回其句柄(一个thread类型的对象);而不会启动它。
调用coroutine.resume函数来执行一个协程。第一次调用coroutine.resume时,第一参数是coroutine.create返回的线程对象,协程就会从这个主函数开始执行。coroutine.resume的其他参数将作为主函数的参数传送过去。协程启动后,将运行到它终止或让出(yield)为止。
终止协程的运行有两种方式:正常的函数返回(显式返回或执行完最后一条指令);非正常的发生一个未捕获错误。在正常结束下,coroutine.resume返回true与协程主函数的返回值,非正常结束下,将返回false与错误信息。
调用coroutine.yield函数可以使协程暂停执行。当一个协程暂停执行时,对应的最近coroutine.resume函数会立即返回,即使这个让出(yield)是发生在一个嵌套函数里面(即不在主函数里,而是直接或间接被主函数调用的函数内)。在一个让出(yield)的情况下,coroutine.resume都是返回true与传给coroutine.yield的参数。在下一次重启这个协程时,它会接着出让(yield)点处继续执行,此时将coroutine.resume的其他参数当做让出点的coroutine.yield返回值。
与coroutine.create相似,coroutine.wrap函数也是创建一个协程,不同的是它返回的不是一个协程对象而是一个函数,当调用这个函数就会启动协程。任何传送过去的参数均被当做coroutine.resume的其他参数看待。coroutine.wrap返回coroutine.resume的除了第一返回值(布尔型的错误码)所有参数。不像coroutine.resume一样会捕捉错误,而coroutine.wrap是会把错误传播给调用者。
下面的代码展示协程是如何工作的:
function foo (a) print("foo", a) return coroutine.yield(2*a) end co = coroutine.create(function (a,b) print("co-body", a, b) local r = foo(a+1) print("co-body", r) local r, s = coroutine.yield(a+b, a-b) print("co-body", r, s) return b, "end" end) print("main", coroutine.resume(co, 1, 10)) print("main", coroutine.resume(co, "r")) print("main", coroutine.resume(co, "x", "y")) print("main", coroutine.resume(co, "x", "y"))
下面是它的执行结果:
co-body 1 10 foo 2 main true 4 co-body r main true 11 -9 co-body x y main true 10 end main false cannot resume dead coroutine
以上是个人对Lua理解后的翻译,如有任何纰漏请大家多多指教