2 – 基本概念

本章描述了语言的基本概念。

2.1 – 值与类型

Lua 是一门动态类型语言。这意味着变量没有类型;只有值才有类型。语言中不设类型定义。所有的值携带自己的类型。

Lua 中所有的值都是 一等公民。这意味着所有的值均可保存在变量中、当作参数传递给其它函数、以及作为返回值。

Lua 中有八种基本类型:nilbooleannumberstringfunctionuserdatathreadtableNil 是值 nil 的类型,其主要特征就是和其它值区别开;通常用来表示一个有意义的值不存在时的状态。Booleanfalsetrue 两个值的类型。nilfalse 都会导致条件判断为假;而其它任何值都表示为真。Number 代表了整数和实数(浮点数)。String 表示一个不可变的字节序列。Lua 对 8 位是友好的:字符串可以容纳任意 8 位值,其中包含零 ('\0') 。Lua 的字符串与编码无关;它不关心字符串中具体内容。

number 类型有两种内部表现方式, 整数浮点数。对于何时使用哪种内部形式,Lua 有明确的规则,但它也按需(参见 §3.4.3)作自动转换。因此,程序员多数情况下可以选择忽略整数与浮点数之间的差异或者假设完全控制每个数字的内部表现方式。标准 Lua 使用 64 位整数和双精度(64 位)浮点数,但你也可以把 Lua 编译成使用 32 位整数和单精度(32 位)浮点数。以 32 位表示数字对小型机器以及嵌入式系统特别合适。(参见 luaconf.h 文件中的宏 LUA_32BITS 。)

Lua 可以调用(以及操作)用 Lua 或 C (参见 §3.4.10)编写的函数。这两种函数有统一类型 function

userdata 类型允许将 C 中的数据保存在 Lua 变量中。用户数据类型的值是一个内存块,有两种用户数据:完全用户数据 ,指一块由 Lua 管理的内存对应的对象;轻量用户数据 ,则指一个简单的 C 指针。用户数据在 Lua 中除了赋值与相等性判断之外没有其他预定义的操作。通过使用 元表 ,程序员可以给完全用户数据定义一系列的操作(参见 §2.4)。你只能通过 C API 而无法在 Lua 代码中创建或者修改用户数据的值,这保证了数据仅被宿主程序所控制。

thread 类型表示了一个独立的执行序列,被用于实现协程(参见 §2.6)。Lua 的线程与操作系统的线程毫无关系。Lua 为所有的系统,包括那些不支持原生线程的系统,提供了协程支持。

table 是一个关联数组,也就是说,这个数组不仅仅以数字做索引,除了 nil 和 NaN 之外的所有 Lua 值都可以做索引。(Not a Number 是一个特殊的数字,它用于表示未定义或表示不了的运算结果,比如 0/0。)表可以是 异构 的;也就是说,表内可以包含任何类型的值( nil 除外)。任何键的值若为 nil 就不会被记入表结构内部。换言之,对于表内不存在的键,都对应着值 nil

表是 Lua 中唯一的数据结构,它可被用于表示普通数组、序列、符号表、集合、记录、图、树等等。对于记录,Lua 使用域名作为索引。语言提供了 a.name 这样的语法糖来替代a["name"] 这种写法以方便记录这种结构的使用。在 Lua 中有多种便利的方式创建表(参见 §3.4.9)。

我们使用 序列 这个术语来表示一个用 {1..n} 的正整数集做索引的表。这里的非负整数 n 被称为该序列的长度(参见 §3.4.7)。

和索引一样,表中每个域的值也可以是任何类型。需要特别指出的是:既然函数是一等公民,那么表的域也可以是函数。这样,表就可以携带 方法 了。(参见 §3.4.11)。

索引一张表的原则遵循语言中的直接比较规则。当且仅当 ij直接比较相等时(即不通过元方法的比较),表达式 a[i]a[j]表示了表中相同的元素。特别指出:一个可以完全表示为整数的浮点数和对应的整数相等(例如:1.0 == 1)。为了消除歧义,当一个可以完全表示为整数的浮点数做为键值时,都会被转换为对应的整数储存。例如,当你写 a[2.0] = true 时,实际被插入表中的键是整数 2 。(另一方面,2 与 "2" 是两个不同的 Lua 值,故而它们可以是同一张表中的不同项。)

表、函数、线程、以及完全用户数据在 Lua 中被称为 对象:变量并不真的 持有 它们的值,而仅保存了对这些对象的 引用。赋值、参数传递、函数返回,都是针对引用而不是针对值的操作,这些操作均不会做任何形式的隐式拷贝。

库函数 type 用于以字符串形式返回给定值的类型。(参见 §6.1)。

2.2 – 环境与全局环境

后面在 §3.2 以及 §3.3.3 会讨论,引用一个叫 var 的自由名字(指在任何层级都未被声明的名字)在句法上都被翻译为 _ENV.var 。此外,每个被编译的 Lua 代码块都会有一个外部的局部变量叫 _ENV(参见 §3.3.2),因此,_ENV 这个名字永远都不会成为一个代码块中的自由名字。

在转译那些自由名字时,_ENV 是否是那个外部的局部变量无所谓。_ENV 和其它你可以使用的变量名没有区别。这里特别指出,你可以定义一个新变量或指定一个参数叫这个名字。当编译器在转译自由名字时所用到的 _ENV ,指的是你的程序在那个点上可见的那个名为 _ENV 的变量。(Lua 的可见性规则参见 §3.5

ENV 用于值的那张表被称为 环境_。

Lua 保有一个被称为 全局环境 特别环境。它被保存在 C 注册表(参见 §4.5)的一个特别索引下。在 Lua 中,全局变量 _G 被初始化为这个值。(_G 不被内部任何地方使用。)

当 Lua 加载一个代码块,ENV 这个上值的默认值就是这个全局环境(参见 load)。因此,在默认情况下,Lua 代码中提及的自由名字都指的全局环境中的相关项(因此,它们也被称为 全局变量_ )。此外,所有的标准库都被加载入全局环境,一些函数也针对这个环境做操作。你可以用 load (或 loadfile)加载代码块,并赋予它们不同的环境。(在 C 里,当你加载一个代码块后,可以通过改变它的第一个上值来改变它的环境。)

2.3 – 错误处理

由于 Lua 是一门嵌入式扩展语言,其所有行为均源于宿主程序中 C 代码对某个 Lua 库函数的调用。(单独使用 Lua 时,lua 程序就是宿主程序。)所以,在编译或运行 Lua 代码块的过程中,无论何时发生错误,控制权都返回给宿主,由宿主负责采取恰当的措施(比如打印错误消息)。

可以在 Lua 代码中调用 error 函数来显式地抛出一个错误。如果你需要在 Lua 中捕获这些错误,可以使用 pcallxpcall保护模式 下调用一个函数。

无论何时出现错误,都会抛出一个携带错误信息的 错误对象错误消息)。Lua 本身只会为错误生成字符串类型的错误对象,但你的程序可以为错误生成任何类型的错误对象,这就看你的 Lua 程序或宿主程序如何处理这些错误对象。

使用 xpcalllua_pcall 时,你应该提供一个 消息处理函数 用于错误抛出时调用。该函数需接收原始的错误消息,并返回一个新的错误消息。它在错误发生后栈尚未展开时调用,因此可以利用栈来收集更多的信息,比如通过探知栈来创建一组栈回溯信息。同时,该处理函数也处于保护模式下,所以该函数内发生的错误会再次触发它(递归)。如果递归太深,Lua 会终止调用并返回一个合适的消息。

2.4 – 元表及元方法

Lua 中的每个值都可以有一个 元表。这个 元表 就是一个普通的 Lua 表,它用于定义原始值在特定操作下的行为。如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。例如,当你对非数字值做加操作时,Lua 会检查该值的元表中的 "__add" 域下的函数。如果能找到,Lua 则调用这个函数来完成加这个操作。

在元表中事件的键值是一个双下划线()加事件名的字符串;键关联的那些值被称为 元方法。在上一个例子中,add 就是键值,对应的元方法是执行加操作的函数。

你可以用 getmetatable 函数来获取任何值的元表。Lua 使用直接访问的方式从元表中查询元方法(参见rawget)。所以,从对象 o 中获取事件 ev 的元方法等价于下面的代码:

  1. rawget(getmetatable(o) or {}, "__ev")

你可以使用 setmetatable 来替换一张表的元表。在 Lua 中,你不可以改变表以外其它类型的值的元表(除非你使用调试库(参见§6.10));若想改变这些非表类型的值的元表,请使用 C API。

表和完全用户数据有独立的元表(当然,多个表和用户数据可以共享同一个元表)。其它类型的值按类型共享元表;也就是说所有的数字都共享同一个元表,所有的字符串共享另一个元表等等。默认情况下,值是没有元表的,但字符串库在初始化的时候为字符串类型设置了元表(参见 §6.4)。

元表决定了一个对象在数学运算、位运算、比较、连接、取长度、调用、索引时的行为。元表还可以定义一个函数,当表对象或用户数据对象在垃圾回收(参见§2.5)时调用它。

对于一元操作符(取负、求长度、位反),元方法调用的时候,第二个参数是个哑元,其值等于第一个参数。这样处理仅仅是为了简化 Lua 的内部实现(这样处理可以让所有的操作都和二元操作一致),这个行为有可能在将来的版本中移除。(使用这个额外参数的行为都是不确定的。)

接下来是元表可以控制的事件的详细列表。每个操作都用对应的事件名来区分。每个事件的键名用加有 '' 前缀的字符串来表示;例如 "add" 操作的键名为字符串 "add"。

  • __add: + 操作。如果任何不是数字的值(包括不能转换为数字的字符串)做加法,Lua 就会尝试调用元方法。首先、Lua 检查第一个操作数(即使它是合法的),如果这个操作数没有为 "__add" 事件定义元方法,Lua 就会接着检查第二个操作数。一旦 Lua 找到了元方法,它将把两个操作数作为参数传入元方法,元方法的结果(调整为单个值)作为这个操作的结果。如果找不到元方法,将抛出一个错误。
  • __sub: - 操作。行为和 "add" 操作类似。
  • __mul: * 操作。行为和 "add" 操作类似。
  • __div: / 操作。行为和 "add" 操作类似。
  • __mod: % 操作。行为和 "add" 操作类似。
  • __pow: ^ (次方)操作。行为和 "add" 操作类似。
  • __unm: - (取负)操作。行为和 "add" 操作类似。
  • __idiv: // (向下取整除法)操作。行为和 "add" 操作类似。
  • __band: & (按位与)操作。行为和 "add" 操作类似,不同的是 Lua 会在任何一个操作数无法转换为整数时(参见 §3.4.3)尝试取元方法。
  • __bor: | (按位或)操作。行为和 "band" 操作类似。
  • __bxor: ~ (按位异或)操作。行为和 "band" 操作类似。
  • __bnot: ~ (按位非)操作。行为和 "band" 操作类似。
  • __shl: << (左移)操作。行为和 "band" 操作类似。
  • __shr: >> (右移)操作。行为和 "band" 操作类似。
  • __concat: .. (连接)操作。行为和 "add" 操作类似,不同的是 Lua 在任何操作数即不是一个字符串也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。
  • __len: # (取长度)操作。如果对象不是字符串,Lua 会尝试它的元方法。如果有元方法,则调用它并将对象以参数形式传入,而返回值(被调整为单个)则作为结果。如果对象是一张表且没有元方法,Lua 使用表的取长度操作(参见 §3.4.7)。其它情况,均抛出错误。
  • __eq: == (等于)操作。和 "add" 操作行为类似,不同的是 Lua 仅在两个值都是表或都是完全用户数据且它们不是同一个对象时才尝试元方法。调用的结果总会被转换为布尔量。
  • __lt: < (小于)操作。和 "add" 操作行为类似,不同的是 Lua 仅在两个值不全为整数也不全为字符串时才尝试元方法。调用的结果总会被转换为布尔量。
  • __le: <= (小于等于)操作。和其它操作不同,小于等于操作可能用到两个不同的事件。首先,像 "lt" 操作的行为那样,Lua 在两个操作数中查找 "le" 元方法。如果一个元方法都找不到,就会再次查找 "lt" 事件,它会假设 a <= b 等价于 not (b < a)。而其它比较操作符类似,其结果会被转换为布尔量。
  • __index: 索引 table[key]。当 table 不是表或是表 table 中不存在key 这个键时,这个事件被触发。此时,会读出 table 相应的元方法。 尽管名字取成这样,这个事件的元方法其实可以是一个函数也可以是一张表。如果它是一个函数,则以 tablekey 作为参数调用它。如果它是一张表,最终的结果就是以 key 取索引这张表的结果。(这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法。)

  • __newindex: 索引赋值 table[key] = value 。和索引事件类似,它发生在table 不是表或是表 table 中不存在key 这个键的时候。此时,会读出 table 相应的元方法。 同索引过程那样,这个事件的元方法即可以是函数,也可以是一张表。如果是一个函数,则以 tablekey、以及 value 为参数传入。如果是一张表,Lua 对这张表做索引赋值操作。(这个索引过程是走常规的流程,而不是直接索引赋值,所以这次索引赋值有可能引发另一次元方法。)

一旦有了 "newindex" 元方法,Lua 就不再做最初的赋值操作。(如果有必要,在元方法内部可以调用 rawset来做赋值。)

  • __call: 函数调用操作 func(args)。当 Lua 尝试调用一个非函数的值的时候会触发这个事件(即 func 不是一个函数)。查找 func 的元方法,如果找得到,就调用这个元方法,func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。

2.5 – 垃圾收集

Lua 采用了自动内存管理。这意味着你不用操心新创建的对象需要的内存如何分配出来,也不用考虑在对象不再被使用后怎样释放它们所占用的内存。Lua 运行了一个 垃圾收集器 来收集所有 死对象(即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、内部结构等,都服从自动管理。

Lua 实现了一个增量标记-扫描收集器。它使用这两个数字来控制垃圾收集循环:垃圾收集器间歇率垃圾收集器步进倍率。这两个数字都使用百分数为单位(例如:值 100 在内部表示 1 )。

垃圾收集器间歇率控制着收集器需要在开启新的循环前要等待多久。增大这个值会减少收集器的积极性。当这个值比 100 小的时候,收集器在开启新的循环前不会有等待。设置这个值为 200 就会让收集器等到总内存使用量达到之前的两倍时才开始新的循环。

垃圾收集器步进倍率控制着收集器运作速度相对于内存分配速度的倍率。增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。不要把这个值设得小于 100 ,那样的话收集器就工作的太慢了以至于永远都干不完一个循环。默认值是 200 ,这表示收集器以内存分配的“两倍”速工作。

如果你把步进倍率设为一个非常大的数字(比你的程序可能用到的字节数还大 10% ),收集器的行为就像一个 stop-the-world 收集器。接着你若把间歇率设为 200 ,收集器的行为就和过去的 Lua 版本一样了:每次 Lua 使用的内存翻倍时,就做一次完整的收集。

你可以通过在 C 中调用 lua_gc或在 Lua 中调用 collectgarbage来改变这俩数字。这两个函数也可以用来直接控制收集器(例如停止它或重启它)。

2.5.1 – 垃圾收集元方法

你可以为表设定垃圾收集的元方法,对于完全用户数据(参见 §2.4),则需要使用 C API 。该元方法被称为 终结器。终结器允许你配合 Lua 的垃圾收集器做一些额外的资源管理工作(例如关闭文件、网络或数据库连接,或是释放一些你自己的内存)。

如果要让一个对象(表或用户数据)在收集过程中进入终结流程,你必须 标记 它需要触发终结器。当你为一个对象设置元表时,若此刻这张元表中用一个以字符串"gc" 为索引的域,那么就标记了这个对象需要触发终结器。注意:如果你给对象设置了一个没有 gc域的元表,之后才给元表加上这个域,那么这个对象是没有被标记成需要触发终结器的。然而,一旦对象被标记,你还是可以自由的改变其元表中的 __gc 域的。

当一个被标记的对象成为了垃圾后,垃圾收集器并不会立刻回收它。取而代之的是,Lua 会将其置入一个链表。在收集完成后,Lua 将遍历这个链表。Lua 会检查每个链表中的对象的 __gc元方法:如果是一个函数,那么就以对象为唯一参数调用它;否则直接忽略它。

在每次垃圾收集循环的最后阶段,本次循环中检测到的需要被回收之对象,其终结器的触发次序按当初给对象作需要触发终结器的标记之次序的逆序进行;这就是说,第一个被调用的终结器是程序中最后一个被标记的对象所携的那个。每个终结器的运行可能发生在执行常规代码过程中的任意一刻。

由于被回收的对象还需要被终结器使用,该对象(以及仅能通过它访问到的其它对象)一定会被 Lua 复活。通常,复活是短暂的,对象所属内存会在下一个垃圾收集循环释放。然后,若终结器又将对象保存去一些全局的地方(例如:放在一个全局变量里),这次复活就持续生效了。此外,如果在终结器中对一个正进入终结流程的对象再次做一次标记让它触发终结器,只要这个对象在下个循环中依旧不可达,它的终结函数还会再调用一次。无论是哪种情况,对象所属内存仅在垃圾收集循环中该对象不可达且没有被标记成需要触发终结器才会被释放。

当你关闭一个状态机(参见 lua_close),Lua 将调用所有被标记了需要触发终结器对象的终结过程,其次序为标记次序的逆序。在这个过程中,任何终结器再次标记对象的行为都不会生效。

2.5.2 – 弱表

弱表 指内部元素为 弱引用 的表。垃圾收集器会忽略掉弱引用。换句话说,如果一个对象只被弱引用引用到,垃圾收集器就会回收这个对象。

一张弱表可以有弱键或是弱值,也可以键值都是弱引用。含有弱值的表允许收集器回收它的值,但会阻止收集器回收它的键。若一张表的键值均为弱引用,那么收集器可以回收其中的任意键和值。任何情况下,只要键或值的任意一项被回收,相关联的键值对都会从表中移除。一张表的元表中的 mode 域控制着这张表的弱属性。当 mode 域是一个包含字符 'k'的字符串时,这张表的所有键皆为弱引用。当 __mode 域是一个包含字符 'v'的字符串时,这张表的所有值皆为弱引用。

属性为弱键强值的表也被称为 暂时表。对于一张暂时表,它的值是否可达仅取决于其对应键是否可达。特别注意,如果表内的一个键仅仅被其值所关联引用,这个键值对将被表内移除。

对一张表的弱属性的修改仅在下次收集循环才生效。尤其是当你把表由弱改强,Lua 还是有可能在修改生效前回收表内一些项目。

只有那些有显式构造过程的对象才会从弱表中移除。值,例如数字和轻量 C 函数,不受垃圾收集器管辖,因此不会从弱表中移除(除非它们的关联项被回收)。虽然字符串受垃圾回收器管辖,但它们没有显式的构造过程,所以也不会从弱表中移除。

弱表针对复活的对象(指那些正在走终结流程,仅能被终结器访问的对象)有着特殊的行为。弱值引用的对象,在运行它们的终结器前就被移除了,而弱键引用的对象则要等到终结器运行完毕后,到下次收集当对象真的被释放时才被移除。这个行为使得终结器运行时得以访问到由该对象在弱表中所关联的属性。

如果一张弱表在当次收集循环内的复活对象中,那么在下个循环前这张表有可能未被正确地清理。

2.6 – 协程

Lua 支持协程,也叫 协同式多线程。一个协程在 Lua 中代表了一段独立的执行线程。然而,与多线程系统中的线程的区别在于,协程仅在显式调用一个让出(yield)函数时才挂起当前的执行。

调用函数coroutine.create可创建一个协程。其唯一的参数是该协程的主函数。create 函数只负责新建一个协程并返回其句柄(一个 thread 类型的对象);而不会启动该协程。

调用coroutine.resume函数执行一个协程。第一次调用coroutine.resume时,第一个参数应传入coroutine.create返回的线程对象,然后协程从其主函数的第一行开始执行。传递给coroutine.resume的其他参数将作为协程主函数的参数传入。协程启动之后,将一直运行到它终止或 让出

协程的运行可能被两种方式终止:正常途径是主函数返回(显式返回或运行完最后一条指令);非正常途径是发生了一个未被捕获的错误。对于正常结束,coroutine.resume 将返回 true,并接上协程主函数的返回值。当错误发生时,coroutine.resume 将返回 false与错误消息。

通过调用coroutine.yield 使协程暂停执行,让出执行权。协程让出时,对应的最近 coroutine.resume 函数会立刻返回,即使该让出操作发生在内嵌函数调用中(即不在主函数,但在主函数直接或间接调用的函数内部)。在协程让出的情况下,coroutine.resume 也会返回 true,并加上传给coroutine.yield 的参数。当下次重启同一个协程时,协程会接着从让出点继续执行。此时,此前让出点处对 coroutine.yield 的调用会返回,返回值为传给coroutine.resume的第一个参数之外的其他参数。

coroutine.create 类似,coroutine.wrap 函数也会创建一个协程。不同之处在于,它不返回协程本身,而是返回一个函数。调用这个函数将启动该协程。传递给该函数的任何参数均当作 coroutine.resume 的额外参数。coroutine.wrap返回coroutine.resume的所有返回值,除了第一个返回值(布尔型的错误码)。和 coroutine.resume 不同,coroutine.wrap不会捕获错误;而是将任何错误都传播给调用者。

下面的代码展示了一个协程工作的范例:

  1. function foo (a)
  2. print("foo", a)
  3. return coroutine.yield(2*a)
  4. end
  5.  
  6. co = coroutine.create(function (a,b)
  7. print("co-body", a, b)
  8. local r = foo(a+1)
  9. print("co-body", r)
  10. local r, s = coroutine.yield(a+b, a-b)
  11. print("co-body", r, s)
  12. return b, "end"
  13. end)
  14.  
  15. print("main", coroutine.resume(co, 1, 10))
  16. print("main", coroutine.resume(co, "r"))
  17. print("main", coroutine.resume(co, "x", "y"))
  18. print("main", coroutine.resume(co, "x", "y"))

当你运行它,将产生下列输出:

  1. co-body 1 10
  2. foo 2
  3. main true 4
  4. co-body r
  5. main true 11 -9
  6. co-body x y
  7. main true 10 end
  8. main false cannot resume dead coroutine

你也可以通过 C API 来创建及操作协程:参见函数lua_newthreadlua_resume,以及 lua_yield