Lua 5.3手册解读 基本概念 一

个人对lua5.3官方manual的翻译,其中穿插了很多自己的理解和大量的意译,并没有完全按照字面翻译。

1 介绍

Lua是一种可扩展的语言,目的是成为一种嵌入式的轻量级的同时功能强大的脚本语言。它对面向对象编程,函数式编程,数据驱动编程都有很好的支持,实现上Lua就是一个纯C实现的库,Lua免费开源,具体相关资料可以在官网找到。

作为一种扩展性语言,lua没有“主程序”的概念,它只能嵌入到宿主程序中运行,宿主程序可以执行一段lua代码,可以读写lua中的变量,可以注册C函数到lua中被lua调用。通过C函数扩展的方式,lua可以被应用到很广泛的领域。Lua发行版里包括了一个很简单的宿主程序的例子(就是编译出来的lua),它使用Lua的库实现了一个可交互使用或者用于批处理作业的Lua解释器。

2 基本概念

2.1 值和类型

Lua是动态类型语言,所以Lua的变量没有类型,只有值才有类型。

Lua的中的值都是first-class values,具体概念详见first-class values wiki

Lua中有八种基本类型:nil, boolean, number, string, function ,userdata, thread, talbenil类型只有一个值nil,通常代表无。boolean类型有两个值truefalse。只有nilfalse在条件判断中代表假,其他都是真。number类型代表整数和浮点数。string类型代表一个不可改的字节序列,每个字节8位,字符串可以包含任意的8位的值。Lua是编码无关的,对字符串的内容没有任何的约束和假设。

Lua内部number类型有两种实现,一个是integer,一个是float。可以显示的区别这两种,如果使用者不区分,Lua也会自动转换。标准Lua使用64位整数,自己更改luaconfg.h中的定义可以更改为32位。

有两种function类型,lua函数和C函数。

类型userdata可以在Lua变量中存储任意的C的数据。一个userdata代表一块内存。有两种userdata:full userdata,light userdata。前者是Lua管理的一块内存对象,后者是一个C指针。userdata只有赋值和id验证的操作,其他的都要通过元表来实现。通过userdata+元表,就可以做到把一个C的对象和其拥有的方法全部引入到lua中,也就是在lua中创建了自己的自定义类型,这个类型的数据结构和方法都是可定制的,所以说Lua可扩展性很好。

类型thread代表一个独立的代码执行流程,通过coroutines来实现,和操作系统那个线程没关系。

类型table大概就是一个关联数组,table的key可以是各种类型,除了nil和NaN,value不限,可以是任何值,nil代表这个key不存在。Lua只有这一种数据结构,只有table,所以非常简单,学习难度小。任何其他想要的数据结构都可以用table间接实现。table的key本着原子相等的原则,float类型如果做key会被转成对应的整数。

如果一个值的类型是一下几种之一,在Lua中这个值有个概念叫对象,:table类型,函数类型,线程类型,full userdata类型。Lua变量并不真正指代对象的值,而是对象的引用。所以赋值,参数传参,函数返回这些操作当作用于Lua对象时,只是在操作引用,并没有任何隐式的值拷贝操作。这点很重要!!默认引用,默认引用,默认引用!!

2.2 环境和全局环境

任何引用了一个未使用名字的变量var,语法上都等于_ENV.var,而且每个Lua chunk 在编译的时候的作用域中都有一个外部局部变量ENV,所以不要用```ENV给自己的变量命名,因为你定义了也是ENV.ENV```。

Lua中任何的_ENVtable都有一个概念叫environment(Lua环境),其实就是一个upvalue。

_G就是一个全局环境,它存在C注册表里,Lua加载一个chunk的时候,默认的环境就是全局环境,所以未使用名字的变量就是“全局变量”了,因为在全局环境的表里。

2.3 错误处理

Lua是嵌入式的,Lua的代码都是由宿主程序启动的,所以Lua块执行报错时,会把控制权移交给宿主程序来决定怎么处理。Lua表示错误的有一种error object,也就是error message,Lua只产生字符串形式的error object给宿主程序。

调用xpcall或者lua_pcall的时候,可以指定一个错误处理函数,这个错误处理函数会在栈展开之前调用,以便收集更多的错误信息。如果这个错误处理函数也报错了,那么会调用自己,有可能出现一个死循环,这种情况下Lua就会打断这个函数然后返回一个Lua认为的合适的错误信息。

2.4 元表和元方法

所有Lua的值都可以有一个元表,其实就是一个普通的表,只是这个表的key代表了一些特殊的操作,比如加,如果一个number类型的值有一个元表其中一个key是__add的话,那么当这个值执行加这个操作的时候,这个元表的key是__add对应的函数就会被调用。

元表里的key都是继承自event,这些key对应的方法就叫元方法。元表其实控制了某个类型的值在一些对应操作下的“表现”。

官方manual有一个关于所有元表事件以及这些事件什么情况下被触发的列表,这里暂时不列出。

2.5 垃圾回收

Lua是有gc的,所以使用者用起来会轻松很多,因为不会考虑内存,大大减少心智负担。

Lua的gc是一种增量式的mark-and-sweep算法。有两个值控制垃圾回收循环,一个是garbage-collector pause,然后是garbage-collector step multiplier,这两个值代表百分比。garbage-collector pause表示垃圾回收器启动新一轮循环前等待的时间,比100小就意味着不等就直接启动新一轮循环,200意味着等到使用的内存变成2倍后再启动新一轮循环。garbage-collector step multiplier控制着回收期相对于内存申请的相对速度。

如果把setp multiplier设置一个非常大的值,回收器的行为就和stop-the-world类型回收器一样,如果再把pause设置成200,那回收器就和以前的Lua中的垃圾回收器做法完全一致了。

2.5.1 gc元方法

如果给table类型或者userdata类型的变量设置gc元方法,那么这些方法在另外一种角度就是析构函数了,这样你就可以和Lua的gc协调合作来释放一些外部资源(比如文件,网络或者数据库连接之类的)。

对于一个Lua对象,你必须在给这个对象设置元表的时候,元表里就指定__gc字段,如果没有而是之后忘元表上加上这个字段是没用的。

gc元方法是这样的:如果一个Lua被标记对象成为了垃圾,Lua并不会马上回收它,而是把它加入到一个列表里,每次gc来的时候,Lua会遍历这个列表,检查每一项的__gc字段,如果这个字段的值是函数就调用这个函数,并把这个对象当作参数传给这个函数,如果不是函数,就忽略它。

gc循环结束后,会倒序执行gc对象的析构函数(如果有的话),析构函数可能会在正常代码执行流程中的任何时候执行。因为gc对象在析构函数中还有可能被用到,所以这些对象并不会被立马释放掉,而是被Lua短暂保存,直到下次gc循环才释放掉这些内存,但是如果析构函数里把对象存到了一些全局的地方,那么就会被永久保存而不释放了。总而言之,只有当gc循环中的对象是不可达状态并且没有析构函数标记的时候才会直接释放掉这个对象的内存。

关闭Lua虚拟机的时候(lua_close),也会逆序调用所有标记的析构函数。

2.5.2 弱表

弱表就是包含弱引用对象的表。弱引用不会gc回收,如果一个对象唯一的引用是一个弱引用,那么gc就会回收这个对象。

弱表可以key是弱引用,value是弱引用,或者两者都是。不管是key还是value被回收,整个键值对都会从表中删除。元表中有个__mode字段来控制。对弱表的“弱”性质的修改并不能立即生效,通常要等到下次gc循环才会生效。

2.6 协程

Lua的一个协程表示一个独立的执行执行序列,并不是多线程系统里的线程(下文中如不特殊说明“线程”均指lua中的线程,而非操作系统里的线程概念)。

coroutine.create创建一个协程,它只接受一个参数作为这个协程的主函数,这个create函数创建一个新的协程并返回一个指向它句柄,句柄类型thread类型,create只创建并不启动。

coroutine.resume启动执行一个协程,第一次调用时第一个参数是coroutine.create函数返回的thread,协程启动后运行自己的主函数,coroutine.resume接受的其他的参数都会传给这个主函数,协程启动后会一直运行直到结束或者显示调用了yield.

协程有两种方式可以结束运行:正常情况是协程的主函数返回了,异常情况就是发生未保护的错误了,正常情况下coroutine.resume返回true外加协程主函数的返回值;发生错误时,coroutine.resume返回false外加错误信息。

coroutine.yield挂起一个协程,当一个协程挂起,对应的coroutine.resume立刻返回,即使是在嵌套的函数调用里发生的挂起操作。一个协程先挂起再调用coroutine.resume的情况,和之前一样,如果正常执行没错误,返回true外加coroutine.yeild传入的参数,第二次恢复同一个协程时,会从上一次挂起的地方继续执行,coroutine.yield会返回coroutine.resume接受的额外参数。

函数coroutine.wrap也会创建协程,但是和coroutine.create不同的是,它并不返回协程的句柄而是返回一个函数,当调用这个函数时,恢复所创建的协程。所有传给coroutine.wrap的额外的参数都会传给coroutine.resumecoroutine.wrap返回coroutine.resume返回的所有返回值,除了第一个那个布尔值。和coroutine.resume不一样的是,coroutine.wrap不捕获错误,任何错误都会传递给调用者。