《Lua编程》 第一篇:语言
形而上 Lv5

Simple is beautiful

(本文使用pdf-craft开源工具和大模型生成整理)

译序

“袁承志知道若再谦逊,那就是瞧人不起,展开五行拳,发拳当胸打去。荣彩和旁观三人本来都以为他武功有独到之秘,哪知使出来的竟是武林中最寻常不过的五行拳。敌对三人登时意存轻视,温青脸上不自禁露出失望的神色。

“荣彩心中暗喜,双拳如风,连抢三下攻势,满拟自己的大力魔爪手江南独步,三四招之间就可破去对方五行拳,那知袁承志轻描淡写的一一化解。再拆数招,荣彩暗暗吃惊,原来对方所使虽是极寻常的拳术,但每一招均是含劲不吐,意在拳先,举手抬足之间隐含极浑厚的内力。”

——金庸 《碧血剑》

编程语言之于程序员,若武功招式之于习武之人,招式虽重要,但在于使用之人。胜者之道,武功只行于表,高手用剑,片草只叶亦威力无穷。

当今武林,派别林立,语言繁杂,林林总总不计其数。主流文化的C/C++、Java、C#、VB;偏安一隅的Fortran;动态语言中的Perl、Tcl、Ruby、Forth、Python,以及本书介绍的Lua;····,等等等等。再加上世界上那些不知道躲在哪的晃的奇奇怪怪的hacker捣鼓出来的异想天开的语言,要想将各类语言囊入怀中,不异于痴人说梦。不信可欣赏一下BrainFuck语言’的Hello World程序,语言本身依如其名。

1
2
3
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-]<.#>+++++++++++[<+++++>-]<.>++++++++[<+
++>-]<.+++.------.--------.[-]>++++++++[<++++>-]<+.[-]+++++
+++++.

虽说语言的威力依使用者本身的修为高低而定,但不同语言本身的设计又有不同。若让用Java 写写操作系统内核、Perl 写写驱动程序、C/C++写写web 应用,都无异于舍近求远,好刀只用上了刀背。

Lua 本身是以简单优雅为本,着眼于处理那些C不擅长的任务。借助C/C++为其扩
展,Lua 可闪现无穷魅力。Lua 本身完全遵循 ANSIC而写成,只要有C 编译器的地方,Lua 便可发挥她的力量。Lua 不需要追求Python 那样的大而全的库,太多的累赘,反而会破坏她的优美。

语言的优美,来自于使用者自己的感悟。Lua 的优雅,也只有使用后才会明白。

扬起帆,让我们一同踏上Lua的学习之旅······

本书的翻译,是www.luachina.net中朋友们共同努力的结果。

注:校对工作在进行。

目录

第一篇 语言
第0章 序言
0.1序言
0.2 Lua的使用者
0.3 Lua的相关资源
0.4 本书的体例
0.5关于本书
0.6感谢
第1章 起点
1.1 Chunks
1.2 全局变量
1.3 词法约定
1.4 命令行方式
第2章 类型和值
2.1 Nil
2.2 Booleans
2.3 Numbers
2.4 Strings
2.5 Functions
2.6 Userdata and Threads
第3章 表达式
3.1 算术运算符
3.2 关系运算符
3.3 逻辑运算符
3.4 连接运算符
3.5优先级
3.6 表的构造
第4章 基本语法
4.1 赋值语句
4.2局部变量与代码块(block)
4.3 控制结构语句
4.4 break和return语句
第5章函数
5.1 返回多个结果值
5.2 可变参数
5.3命名参数
第6章 再论函数
6.1闭包
6.2非全局函数
6.3正确的尾调用(Proper TailCalls)
第7章 迭代器与泛型for
7.1 迭代器与闭包
7.2 范性for的语义
7.3无状态的迭代器
7.4 多状态的迭代器
7.5 真正的迭代器
第8章编译·运行·调试
8.1 require函数
8.2 C Packages
8.3 错误
8.4 异常和错误处理
8.5错误信息和回跟踪(Tracebacks)
第9章 协同程序
9.1 协同的基础
9.2管道和过滤器
9.3用作迭代器的协同
9.4 非抢占式多线程
第10章 完整示例
10.1 Lua作为数据描述语言使用
10.2 马尔可夫链算法

第0章序言

本章包括作者的序言、文章的体例(convention)以及其它一些“每本书开头都会的内容”

0.1序言

目前很多程序语言都专注于帮你编写成千上万行的代码,所以此类型的语言所提供的包、命名空间、复杂的类型系统及无数的结构,有上千页的文档需要操作者学习。

而Lua 并不帮你编写大量的代码的程序,相反的,Lua 仅让你用少量的代码解决关键问题。为实现这个目标,像其他语言一样Lua 依赖于其可扩展性。但是与其他语言不同的是,不仅用Lua 编写的软件易于扩展,而且用其他语言比如C/C++编写的软件也很容易使用Lua扩展其功能。

一开始,Lua 就被设计成很容易和传统的C/C++整合的语言。这种语言的二元性带来了极大的好处。Lua 是一个小巧而简单的语言,因为Lua 不致力于做C语言已经做得这些任务。Lua 所提供的机制是C不善于的:高级语言、动态结构、简洁、易于测试和调试等。正因为如此,Lua 具有良好的安全保证,自动内存管理,简便的字符串处理功能及其他动态数据的改变。

Lua 不仅是一种易于扩展的语言,也是一种易整合语言(glue language);Lua 支持情况下,组件使用像C/C++等静态的语言编写。但Lua 是我们整合各个组件的粘合剂。又通常情况下,组件(或对象)表现为具体在程序开发过程中很少变化的、占用大量CPU时间的决定性的程序,例如窗口部件和数据结构。对那种在产品的生命周期内变化比较多的应用方向使用Lua可以更方便的适应变化。除了作为整合语言外,Lua自身也是一个功能强大的语言。Lua 不仅可以整合组件,还可以编辑组件甚至完全使用Lua 创建组件。

除了Lua 外,还有很多类似的脚本语言,例如:Perl、Tcl、Ruby、Forth、Python。虽然其他语言在某些方面与Lua 有着共同的特色,但下面这些特征是Lua 特有的:

① 可扩展性。Lua 的扩展性非常卓越,以至于很多人把 Lua 用作搭建领域语言的工具(注:比如游戏脚本)。Lua 被设计为易于扩展的,可以通过Lua 代码或者C代码扩展,Lua 的很多功能都是通过外部库来扩展的。Lua 很容易与 C/C++、java、fortran、Smalltalk、Ada,以及其他语言接口。

②简单。Lua 本身简单,小巧;内容少但功能强大,这使得Lua 易于学习,很容易实现一些小的应用。他的完全发布版(代码、手册以及某些平台的二进制文件)!!仅用一张软盘就可以装得下。

③高效率。Lua 有很高的执行效率,统计表明 Lua 是目前平均效率最高的脚本语言。

④与平台无关。Lua 几乎可以运行在所有我们听说过的系统上,如 NextStep、OS/2、PlayStation II (Sony)、Mac OS-9、OS X、BeOS、MS-DOS、IBMmainframes、EPOC、PalmOS、MCF5206eLITE Evaluation Board、RISCOS,及所有的 Windows 和 Unix。Lua 不是通过使用条件编译实现平台无关,而是完全使用 ANSI(ISO)C,这意味着只要你有ANSIC 编译器你就可以编译并使用Lua。

Lua 大部分强大的功能来自于他的类库,这并非偶然。Lua 的长处之一就是可以通过新类型和函数来扩展其功能。动态类型检查最大限度允许多态出现,并自动简化调用内存管理的接口,因为这样不需要关心谁来分配内存谁来释放内存,也不必担心数据溢出。高级函数和匿名函数均可以接受高级参数,使函数更为通用。

Lua自带一个小规模的类库。在受限系统中使用Lua,如嵌入式系统,我们可以有留需要的函数。记住:Lua是很小的(即使加上全部的标准库)并且在大部分系统下你仍可以不用担心的使用全部的功能。

0.2 Lua的使用者

Lua 使用者分为三大类:使用 Lua 嵌入到其他应用中的、独立使用Lua 的、将 Lua和C混合使用的。

第一:很多人使用Lua 嵌入在应用程序,比如CGILua(搭建动态网页)、LuaOrb(访问CORBA对象。这些类型用Lua-API注册新函数,创建新类型,通过配置Lua 就可以改变应用宿主语言的行为。通常,这种应用的使用者并不知道Lua是一种独立的语言。例如:CGILua用户一般会认为Lua是一种用于Web 的语言。

第二:作为一种独立运行的语言,Lua 也是很有用的,主要用于文本处理或者只运行一次的小程序。这种应用Lua主要使用它的标准库来实现,标准库提供模式匹配和其它一些字串处理的功能。我们可以这样认为:Lua是文本处理领域的嵌入式语言。

第三:还有一些使用者使用其他语言开发,把Lua 当作库使用。这些人大多使用C语言开发,但使用Lua建立简单灵活易于使用的接口。

本书面向以上三类读者。书的第一部分阐述了语言的本身,展示语言的潜在功能。我们讲述了不同的语言结构,并用一些例子展示如何解决实际问题。这部分既包括基本的语言的控制结构,也包括高级的迭代子和协同。

第二部分重点放在 Lua 特有的数据结构一—tables 上,讨论了数据结构、持久性、包及面向对象编程,这里我们将看到Lua的真正强大之处。

第三部分介绍标准库。每个标准库一章:数学库、table 库、string 库、ⅡO 库、OS库、Debug 库。

最后一部分介绍了 Lua 和 C 接口的 API,这部分介绍在C语言中开发应用而不是Lua 中,应用对于那些打算将Lua 嵌入到C/C++中的读者可能会对此部分更感兴趣。

0.3 Lua的相关资源

如果你真得想学一门语言,参考手册是必备的。本书和Lua参考手册互为补充,手册仅仅描述语言本身,因此他既不会告诉你语言的数据结构也不会举例说明,但手册是Lua 的权威性文档,http://www.lua.org 可以得到手册的内容。

另外本书仅针对Lua 5.0,如果你的版本不同,请查阅Lua 手册或者比较版本间的差异。

0.4本书的体例

<1>字符串使用双引号,比如”literal strings”;单字符使用单引号,比如’a’;模式串也是用单引号,比如’[%w_]*’。

<2> 符号–>表示语句的输出或者表达式的结果:

<3>符号<–>表示等价,即对于 Lua 来说,用 this 与 that 没有区别。

0.5关于本书

开始打算写这本书是1998 年冬天(南半球),那时候Lua 版本是3.1;2000 年v4.0;2003 年 v5.0。

很明显的是,这些变化给本书带来很大的冲击,有些内容失去了它存在理由,比如关于超值(upvalues)的复杂的解释。一些章节被重写,比如CAPI,另外一些章节被增加进来,比如协同处理。

不太明显的是,Lua 语言本身的发展对本书的完成也产生了很大的影响。一些语言的变化在本书中并没有被涵盖进来,这并非偶然的。在本书的创作过程中,有的时候在当你想尽力去解释清楚如何使用的前提是你应该觉得使用这个东西很容易,这表明Lua某些地方需要被改进。还有的时候,我顺利的写完某个章节,结果却是没有人能看得懂我写的或者没有人对我在这个章节内表达的观点达成一致。大部分情况下,这是我的过错因为我是个作家,偶尔我也会因此发现语言本身的一些需要改进的缺陷(举例来说,从upvalues 到lexical scoping 的转变是由无意义的尝试所带来的抱怨所引发的,在此书的先前的草稿里,把 upvalues 形容成是lexical scoping 的一种)。

本书的完成必须服从语言的变化,本书在这个时候完成的原因:

<1>Lua5.0是一个成熟的版本

<2>语言变得越来越大,超出了最初本书的目标。此外一个原因是我迫切的想将Lua 介绍给大家让更多的人了解Lua

0.6感谢

在完成本书的过程中,很多人给了我极大的帮助:

Luiz Henrique de Figueiredo 和 Waldemar Celes 给了我很大的帮助,使得本书能够更好完成,LuizHenrique 也帮助设计了本书的内部。

Noemi Rodriguez,André Carregal, Diego Nehab,以及 Gavin Wraith 阅读了本书的草稿提出了很多有价值的建议。

Renato Cerqueira, Carlos Cassino, Tomas Guisasola, Joe Myers 和 Ed Ferguson 也提出了很多重要的建议。

Alexandre Nakonechnyj负责本书的封面和内部设计。

Rosane Teles 负责CIP数据的准备。

谢谢他们所有人。

第1章起点

写一个最最简单的程序—Hello World。

1
print("Hello World")

假定你把上面这句保存在 hello.lua 文件中,你在命令行只需要:

1
prompt> lua hello.lua 

让我们来看一个稍微复杂点的例子:

这个例子定义了一个函数,计算输入参数n 的阶乘;本例要求用户输入一个数字 n,然后打印 n 的阶乘。

1.1 Chunks

Chunk 是一系列语句,Lua 执行的每一块语句,比如一个文件或者交互模式下的每-行都是一个Chunk。

每个语句结尾的分号(;)是可选的,但如果同一行有多个语句最好用;分开

1
a = 1   b = a*2    -- ugly, but valid 

一个Chunk 可以是一个语句,也可以是一系列语句的组合,还可以是函数,Chunk可以很大,在Lua 中几个 MByte 的 Chunk是很常见的。

你还可以以交互模式运行Lua,不带参数运行Lua:

1
2
Lua 5.0  Copyright © 1994-2003 Tecgraf, PUC-Rio 
>

你键入的每个命令(比如:”Hello World”)在你键入回车之后立即被执行,键入文件结束符可以退出交互模式(Ctrl-D in Unix, Ctrl-Z in DOS/Windows),或者调用OS库的os.exit()函数也可以退出。

在交互模式下,Lua 通常把每一个行当作一个Chunk,但如果Lua一行不是一个完整的Chunk 时,他会等待继续输入直到得到一个完整的Chunk.在Lua 等待续行时,显示不同的提示符(一般是>>).

可以通过指定参数让Lua 执行一系列Chunk。例如:假定一个文件 a 内有单个语句x=1;另一个文件b 有语句 print(x)

1
prompt> lua -la -lb

命令首先在一个 Chunk 内先运行 a 然后运行 b。(注意:-1选项会调用 require,将会在指定的目录下搜索文件,如果环境变量没有设好,上面的命令可能不能正确运行。我们将在 8.1节详细更详细的讨论 the require function)

-i选项要求Lua运行指定Chunk后进入交互模式

1
prompt> lua -i -la -lb

将在一个 Chunk 内先运行 a 然后运行b,最后直接进入交互模式。

另一个连接外部 Chunk 的方式是使用 dofile 函数,dofile 函数加载文件并执行它.假设有一个文件:

1
2
3
4
5
6
7
8
9
-- file 'lib1.lua' 
function norm (x, y)
local n2 = x^2 + y^2
return math.sqrt(n2)
end

function twice (x)
return 2*x
end

在交互模式下:

1
2
3
> dofile("lib1.lua")  -- load your library 
> n = norm(3.4, 1.0)
> print(twice(n)) --> 7.0880180586677

-i 和 dofile 在调试或者测试 Lua 代码时是很方便的。

1.2全局变量

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil.

1
2
3
print(b)  --> nil 
b = 10
print(b) --> 10

如果你想删除一个全局变量,只需要将变量负值为 nil

1
2
b = nil 
print(b) --> nil

这样变量 b 就好像从没被使用过一样.换句话说,当且仅当一个变量不等于 nil 时,这个变量存在。

1.3词法约定

标示符:字母(letter)或者下划线开头的字母、下划线、数字序列.最好不要使用下划线加大写字母的标示符,因为Lua 的保留字也是这样的。Lua 中,letter 的含义是依赖于本地环境的。

保留字:以下字符为Lua 的保留字,不能当作标识符。

注意:Lua是大小写敏感的.

注释:单行注释:–

多行注释:–[[ –]]

1.4命令行方式

lua [options] [script [args]]

-e:直接将命令传入Lua

1
prompt> lua -e "print(math.sin(12))" --> -0.53657291800043

-l:加载一个文件.

-i:进入交互模式.

_PROMPT内置变量作为交互模式的提示符

1
2
prompt> lua -i -e "_PROMPT=' lua> '" 
lua>

Lua 的运行过程,在运行参数之前,Lua 会查找环境变量 LUA_INIT 的值,如果变量存在并且值为@filename,Lua 将加载指定文件。如果变量存在但不是以@开头,Lua假定 filename 为 Lua 代码文件并且运行他。利用这个特性,我们可以通过配置,灵活的设置交互模式的环境。可以加载包,修改提示符和路径,定义自己的函数,修改或者重名名函数等。

全局变量 arg 存放 Lua 的命令行参数。

1
prompt> lua script a b c 

在运行以前,Lua 使用所有参数构造 arg 表。脚本名索引为O,脚本的参数从1开始增加。脚本前面的参数从-1开始减少。

1
prompt> lua -e "sin=math.sin" script a b 

arg 表如下:

1
2
3
4
5
6
arg[-3] = "lua" 
arg[-2] = "-e"
arg[-1] = "sin=math.sin"
arg[0] = "script"
arg[1] = "a"
arg[2] = "b"

第2章 类型和值

Lua 是动态类型语言,变量不要类型定义。Lua 中有8个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。函数 type 可以测试给定变量或者值的类型。

变量没有预定义的类型,每一个变量都可能包含任一种类型的值。

注意上面最后两行,我们可以使用 function 像使用其他值一样使用(更多的介绍参考第六章)。一般情况下同一变量代表不同类型的值会造成混乱,最好不要用,但是特殊情况下可以带来便利,比如nil。

2.1 Nil

Lua 中特殊的类型,他只有一个值:nil; ;一个全局变量没有被赋值以前默认值为 nil;给全局变量负 nil 可以删除该变量。

2.2 Booleans

两个取值 false 和 true。但要注意Lua 中所有的值都可以作为条件。在控制结构的条件中除了false 和 nil为假,其他值都为真。所以Lua认为O和空串都是真。

2.3 Numbers

表示实数,Lua 中没有整数。一般有个错误的看法CPU运算浮点数比整数慢。事实不是如此,用实数代替整数不会有什么误差(除非数字大于100,000,000,000,000)。Lua的 numbers 可以处理任何长整数不用担心误差。你也可以在编译Lua 的时候使用长整型或者单精度浮点型代替 numbers,在一些平台硬件不支持浮点数的情况下这个特性是非常有用的,具体的情况请参考Lua发布版所附的详细说明。和其他语言类似,数字常量的小数部分和指数部分都是可选的,数字常量的例子:

2.4 Strings

指字符的序列。lua是8位字节,所以字符串可以包含任何数值字符,包括嵌入的0。这意味着你可以存储任意的二进制数据在一个字符串里。Lua 中字符串是不可以修改的,你可以创建一个新的变量存放你要的字符串,如下:

string 和其他对象一样,Lua 自动进行内存分配和释放,一个 string 可以只包含一个字母也可以包含一本书,Lua 可以高效的处理长字符串,1M 的 string 在Lua 中是很常见的。可以使用单引号或者双引号表示字符串

1
2
a = "a line"
b = 'another line'

为了风格统一,最好使用一种,除非两种引号嵌套情况。对于字符串中含有引号的情况还可以使用转义符来表示。Lua 中的转义序列有:


例子:

1
2
3
4
5
6
7
8
> print("one line\nnext line\n\"in quotes\", 'in quotes'") 
one line
next line
"in quotes", 'in quotes'
> print('a backslash inside quotes: \'\\\'')
a backslash inside quotes: '\'
> print("a simpler way: '\\'")
a simpler way: '\'

还可以在字符串中使用\ddd(ddd 为三位十进制数字)方式表示字母。

“alo\n123"“和\97lo\10\04923”是相同的。

还可以使用[[…]]表示字符串。这种形式的字符串可以包含多行也,可以嵌套且不会解释转义序列,如果第一个字符是换行符会被自动忽略掉。这种形式的字符串用来包含一段代码是非常方便的。

运行时,Lua 会自动在 string 和 numbers 之间自动进行类型转换,当一个字符串使用算术操作符时,string 就会被转成数字。

1
2
3
4
print("10" + 1)    --> 11 
print("10 + 1") --> 10 + 1
print("-5.3e - 10" * "2") --> -1.06e-09
print("hello" + 1) -- ERROR (cannot convert "hello")

反过来,当Lua 期望一个 string 而碰到数字时,会将数字转成 string。

..在Lua 中是字符串连接符,当在一个数字后面写..时,必须加上空格以防止被解释错。

尽管字符串和数字可以自动转换,但两者是不同的,像10==”10”这样的比较永远都是错的。如果需要显式将 string 转成数字可以使用函数tonumberO,如果 string 不是正确的数字该函数将返回 nil。

反之,可以调用 tostring()将数字转成字符串,这种转换一直有效:

1
2
print(tostring(10) == "10")  --> true 
print(10 .. "" == "10") --> true

2.5 Functions

函数是第一类值(和其他变量相同),意味着函数可以存储在变量中,可以作为函数的参数,也可以作为函数的返回值。这个特性给了语言很大的灵活性:一个程序可以重新定义函数增加新的功能或者为了避免运行不可靠代码创建安全运行环境而隐藏函数,此外这特性在Lua 实现面向对象中也起了重要作用(在第16章详细讲述)。

Lua 可以调用 lua 或者C 实现的函数,Lua 所有标准库都是用C 实现的。标准库包括 string 库、table 库、IO库、OS 库、算术库、debug 库。

2.6 Userdata and Threads

userdata 可以将C数据存放在Lua 变量中,userdata 在Lua 中除了赋值和相等比较外没有预定义的操作。userdata 用来描述应用程序或者使用C 实现的库创建的新类型。例如:用标准IO 库来描述文件。下面在CAPI章节中我们将详细讨论。

在第九章讨论协同操作的时候,我们介绍线程。

第3章表达式

Lua 中的表达式包括数字常量、字符串常量、变量、一元和二元运算符、函数调用。还可以是非传统的函数定义和表构造。

3.1算术运算符

二元运算符:+-*/~(加减乘除幂)

元运算符:- (负值)

这些运算符的操作数都是实数。

3.2关系运算符

1
<   >   <=  >=  ==  ~=

这些操作符返回结果为 false 或者 true;==和~=比较两个值,如果两个值类型不同,Lua 认为两者不同;nil 只和自己相等。Lua 通过弓用比较 tables、userdata、functions。也就是说当且仅当两者表示同一个对象时相等。

1
2
3
4
a = {}; a.x = 1; a.y = 0 
b = {}; b.x = 1; b.y = 0
c = a
a==c but a~=b

Lua 比较数字按传统的数字大小进行,比较字符串按字母的顺序进行,但是字母顺序依赖于本地环境。

当比较不同类型的值的时候要特别注意:

为了避免不一致的结果,混合比较数字和字符串,Lua 会报错,比如:2<”15”

3.3逻辑运算符

and or not

逻辑运算符认为 false 和 nil是假(false),其他为真,O也是 true.

and 和 or 的运算结果不是 true 和 false,而是和它的两个操作数相关。

1
2
a and b  -- 如果a为false,则返回a,否则返回b 
a or b -- 如果a为true,则返回a,否则返回b

例如:

1
2
3
4
5
print(4 and 5)   --> 5 
print(nil and 13) --> nil
print(false and 13) --> false
print(4 or 5) --> 4
print(false or 5) --> 5

个很实用的技巧:如果x为 false 或者 nil 则给x赋初始值 v

1
x = x or v

等价于

1
2
3
if not x then 
x = v
end

and 的优先级比 or 高。

C语言中的三元运算符

1
a?b:c

在Lua 中可以这样实现:

1
(a and b) or c

not 的结果一直返回 false 或者 true

1
2
3
4
print(not nil)   --> true 
print(not false) --> true
print(not 0) --> false
print(not not nil) --> false

3.4连接运算符

1
.. --两个点

字符串连接,如果操作数为数字,Lua将数字转成字符串。

3.5优先级

从高到低的顺序:

除了~和..外所有的二元运算符都是左连接的。

3.6表的构造

构造器是创建和初始化表的表达式。表是Lua特有的功能强大的东西。最简单的构造函数是,用来创建一个空表。可以直接初始化数组:

不管用何种方式创建table,我们都可以向表中添加或者删除任何类型的域,构造函数仅仅影响表的初始化。

每次调用构造函数,Lua 都会创建一个新的 table,可以使用 table 构造一个 list:

1
2
3
4
list = nil 
for line in io.lines() do
list = {next=list, value=line}
end

这段代码从标准输入读进每行,然后反序形成链表。下面的代码打印链表的内容:

1
2
3
4
5
l = list 
while l do
print(l.value)
l = l.next
end

在同一个构造函数中可以混合列表风格和record 风格进行初始化,如:

1
2
3
4
5
6
polyline = {color="blue", thickness=2, npoints=4, 
{x=0, y=0},
{x=-10, y=0},
{x=-10, y=1},
{x=0, y=1}
}

这个例子也表明我们可以嵌套构造函数来表示复杂的数据结构

1
print(polyline[2].x) --> -10

上面两种构造函数的初始化方式还有限制,比如你不能使用负索引初始化一个表中元素,字符串索引也不能被恰当的表示。下面介绍一种更一般的初始化方式,我们用[expression]显示的表示将被初始化的索引:

1
2
3
4
5
6
opnames = {["+"] = "add", ["-"] = "sub", 
["*"] = "mul", ["/"] = "div"}
i = 20; s = "-"
a = {[i+0] = s, [i+1] = s..s, [i+2] = s..s..s}
print(opnames[s]) --> sub
print(a[22]) --> ---

list 风格初始化和 record 风格初始化是这种一般初始化的特例:

1
2
3
4
5
6
{x=0, y=0} 
<-->
{["x"]=0, ["y"]=0}
{"red", "green", "blue"}
<-->
{[1]="red", [2]="green", [3]="blue"}

如果真的想要数组下标从0开始:

1
2
days = {[0]="Sunday", "Monday", "Tuesday", "Wednesday", 
"Thursday", "Friday", "Saturday"}

注意:不推荐数组下标从0开始,否则很多标准库不能使用。

在构造函数的最后的”,”是可选的,可以方便以后的扩展。

1
a = {[1]="red", [2]="green", [3]="blue",} 

在构造函数中域分隔符逗号(“,”)可以用分号(“;”)替代,通常我们使用分号用来分割不同类型的表元素。

1
{x=10, y=45; "one", "two", "three"} 

第4章基本语法

Lua 像C和PASCAL几乎支持所有的传统语句:赋值语句、控制结构语句、函数调用等,同时也支持非传统的多变量赋值、局部变量声明。

4.1赋值语句

赋值是改变一个变量的值和改变表域的最基本的方法。

Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。

1
a, b = 10, 2*x  <-->  a=10; b=2*x

遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:

1
2
x, y = y, x      -- swap 'x' for 'y' 
a[i], a[j] = a[j], a[i] -- swap 'a[i]' for 'a[i]'

当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:

1
2
a. 变量个数>值的个数    按变量个数补足nil 
b. 变量个数<值的个数 多余的值会被忽略

例如:

1
2
3
4
5
6
7
8
a, b, c = 0, 1  
print(a,b,c) --> 0 1 nil

a, b = a+1, b+1, b+2 -- value of b+2 is ignored
print(a,b) --> 1 2

a, b, c = 0
print(a,b,c) --> 0 nil nil

上面最后一个例子是一个常见的错误情况,注意:如果要对多个变量赋值必须依次对每个变量赋值。

1
a, b, c = 0, 0, 0 

f()返回两个值,第一个赋给a,第二个赋给b。

4.2局部变量与代码块(block)

使用local 创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。代码块:指一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)。

注意,如果在交互模式下上面的例子可能不能输出期望的结果,因为第二句locali=l第二句的i已经超出了他的有效范围。可以将这段代码放在 do.end(相当于c/c++的{})块中。

应该尽可能的使用局部变量,有两个好处:

  1. 避免命名冲突
  2. 访问局部变量的速度比全局变量更快

我们给 block 划定一个明确的界限:do..end 内的部分。当你想更好的控制局部变量的作用范围的时候这是很有用的。

4.3控制结构语句

控制结构的条件表达式结果可以是任何值,Lua 认为 false 和 nil 为假,其他值为真。

if语句,有三种形式:

while 语句:

1
2
3
while condition do 
statements;
end;

repeat-until 语句:

for 语句有两大类:第一,数值 for 循环:

1
2
3
for var=exp1,exp2,exp3 do 
loop-part
end

for 将用 exp3 作为 step 从 expl(初始值)到 exp2(终止值),执行 loop-part。其中exp3 可以省略,默认 step=1

有几点需要注意:

  1. 三个表达式只会被计算一次,并且是在循环开始前。
1
2
3
4
5
6
7
for i=1,f(x) do 
print(i)
end

for i=10,1,-1 do
print(i)
end

第一个例子f(x)只会在循环前被调用一次。

  1. 控制变量var 是局部变量自动被声明,并且只在循环内有效.
1
2
3
4
for i=1,10 do 
print(i)
end
max = i -- probably wrong! 'i' here is global

如果需要保留控制变量的值,需要在循环中将其保存

1
2
3
4
5
6
7
8
9
-- find a value in a list 
local found = nil
for i=1,a.n do
if a[i] == value then
found = i -- save value of 'i'
break
end
end
print(found)
  1. 循环过程中不要改变控制变量的值,那样做的结果是不可预知的。如果要退出循环,使用 break 语句。

第二, 范型 for 循环:

前面已经见过一个例子:

1
2
-- print all values of array 'a'  
for i,v in ipairs(a) do print(v) end

范型for遍历迭代子函数返回的每一个值。
再看一个遍历表key 的例子:

1
2
-- print all keys of table 't' 
for k in pairs(t) do print(k) end

范型for 和数值for 有两点相同:

  1. 控制变量是局部变量
  2. 不要修改控制变量的值

再看一个例子,假定有一个表:

1
2
days = {"Sunday", "Monday", "Tuesday", "Wednesday", 
"Thursday", "Friday", "Saturday"}

现在想把对应的名字转换成星期几,一个有效地解决问题的方式是构造一个反向表:

1
2
3
4
revDays = {["Sunday"] = 1, ["Monday"] = 2,  
["Tuesday"] = 3, ["Wednesday"] = 4,
["Thursday"] = 5, ["Friday"] = 6,
["Saturday"] = 7}

下面就可以很容易获取问题的答案了:

1
2
x = "Tuesday"  
print(revDays[x]) --> 3

我们不需要手工,可以自动构造反向表

1
2
3
revDays = {}  
for i,v in ipairs(days) do
end

如果你对范型for还有些不清楚在后面的章节我们会继续来学习。

4.4 break和 return 语句

break语句用来退出当前循环(for,repeat,while)。在循环外部不可以使用。

return 用来从函数返回结果,当一个函数自然结束结尾会有一个默认的 return。(这种函数类似pascal的过程)

Lua 语法要求 break 和 return 只能出现在 block 的结尾一句(也就是说:作为 chunk的最后一句,或者在 end 之前,或者 else 前,或者 until 前),例如:

1
2
3
4
5
local i = 1 
while a[i] do
if a[i] == v then break end
i = i + 1
end

有时候为了调试或者其他目的需要在 block 的中间使用 return 或者 break,可以显式的使用 do..end 来实现:

第5章函数

函数有两种用途:1.完成指定的任务,这种情况下函数作为调用语句使用;2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。

语法:

1
2
3
function func_name (arguments-list) 
statements-list;
end;

调用函数的时候,如果参数列表为空,必须使用()表明是函数调用。

上述规则有一个例外,当函数只有一个参数并且这个参数是字符串或者表构造的时候,()是可选的:

Lua 也提供了面向对象方式调用函数的语法,比如 o:foo(x)与 o.foo(o,x)是等价的,后面的章节会详细介绍面向对象内容。

Lua 使用的函数可以是Lua 编写也可以是其他语言编写,对于Lua程序员来说用什么语言实现的函数使用起来都一样。

Lua 函数实参和形参的匹配与赋值语句类似,多余部分被忽略,缺少部分用 nil补足。

5.1返回多个结果值

Lua 函数可以返回多个结果值,比如 string.find,其返回匹配串“开始和结束的下标”(如果不存在匹配串返回 nil)。

1
2
s, e = string.find("hello Lua users", "Lua") 
print(s, e) --> 7 9

Lua 函数中,在return 后列出要返回的值得列表即可返回多值,如:

Lua总是调整函数返回值的个数去适用调用环境,当作为一个语句调用函数时,所有返回值被忽略。假设有如下三个函数:

第一,当作为表达式调用函数时,有以下几种情况:

  1. 当调用作为表达式最后一个参数或者仅有一个参数时,根据变量个数函数尽可能多地返回多个值,不足补 nil,超出舍去。

  2. 其他情况下,函数调用仅返回第一个值 (如果没有返回值为 nil)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x,y = foo2()    -- x='a', y='b' 
    x = foo2() -- x='a', 'b' is discarded
    x,y,z = 10,foo2() -- x=10, y='a', z='b'

    x,y = foo0() -- x=nil, y=nil
    x,y = foo1() -- x='a', y=nil
    x,y,z = foo2() -- x='a', y='b', z=nil

    x,y = foo2(), 20 -- x='a', y=20
    x,y = foo0(), 20, 30 -- x='nil', y=20, 30 is discarded

第二,函数调用作为函数参数被调用时,和多值赋值是相同。

第三,函数调用在表构造函数中初始化时,和多值赋值时相同。

1
2
3
4
5
a = {foo0()}    -- a = {}    (an empty table) 
a = {foo1()} -- a = {'a'}
a = {foo2()} -- a = {'a', 'b'}

a = {foo0(), foo2(), 4} -- a[1] = nil, a[2] = 'a', a[3] = 4

另外,return f()这种类型的返回 f()返回的所有值

1
2
3
4
5
6
7
8
9
10
11
function foo (i) 
if i == 0 then return foo0()
elseif i == 1 then return foo1()
elseif i == 2 then return foo2()
end
end

print(foo(1)) --> a
print(foo(2)) --> a b
print(foo(0)) -- (no results)
print(foo(3)) -- (no results)

可以使用圆括号强制使调用返回一个值。

1
2
3
print((foo0()))  --> nil 
print((foo1())) --> a
print((foo2())) --> a

一个return 语句如果使用圆括号将返回值括起来也将导致返回一个值。

函数多值返回的特殊函数unpack,接受一个数组作为输入参数,返回数组的所有元素。unpack被用来实现范型调用机制,在C语言中可以使用函数指针调用可变的函数,可以声明参数可变的函数,但不能两者同时可变。在Lua 中如果你想调用可变参数的可变函数只需要这样:

1
f(unpack(a)) 

unpack返回a所有的元素作为f()的参数

预定义的 unpack 函数是用C语言实现的,我们也可以用 Lua来完成:

5.2可变参数

Lua 函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(..)表示函数有可变的参数。Lua 将函数的参数放在一个叫 arg 的表中,除了参数以外,arg表中还有一个域n表示参数的个数。

例如,我们可以重写 print 函数:

有时候我们可能需要几个固定参数加上可变参数

1
2
3
4
5
6
7
function g (a, b, ...) end 

CALL PARAMETERS

g(3) a=3, b=nil, arg={n=0}
g(3, 4) a=3, b=4, arg={n=0}
g(3, 4, 5, 8) a=3, b=4, arg={5, 8; n=2}

如上面所示,Lua 会将前面的实参传给函数的固定参数,后面的实参放在 arg 表中。举个具体的例子,如果我们只想要 string.find 返回的第二个值:

个典型的方法是使用虚变量 (下划线)

1
2
3
local _, x = string.find(s, p) 
-- now use 'x'
...

还可以利用可变参数声明一个 select 函数:

有时候需要将函数的可变参数传递给另外的函数调用,可以使用前面我们说过的unpack(arg)返回 arg 表所有的可变参数,Lua 提供了一个文本格式化的函数 string.format(类似 C语言的 sprintf 函数):

1
2
3
function fwrite(fmt, ...) 
return io.write(string.format(fmt, unpack(arg)))
end

这个例子将文本格式化操作和写操作组合为一个函数。

5.3命名参数

Lua 的函数参数是和位置相关的,调用时实参会按顺序依次传给形参。有时候用名字指定参数是很有用的,比如rename 函数用来给一个文件重命名,有时候我们我们记不清命名前后两个参数的顺序了:

1
2
-- invalid code 
rename(old="temp.lua", new="temp1.lua")

上面这段代码是无效的,Lua 可以通过将所有的参数放在一个表中,把表作为函数的唯一参数来实现上面这段伪代码的功能。因为Lua语法支持函数调用时实参可以是表的构造。

1
rename{old="temp.lua", new="templ.lua"}

根据这个想法我们重定义了rename:

1
2
3
function rename (arg) 
return os.rename(arg.old, arg.new)
end

当函数的参数很多的时候,这种函数参数的传递方式很方便的。例如GUI库中创建窗体的函数有很多参数并且大部分参数是可选的,可以用下面这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
w = Window { 
x=0, y=0, width=300, height=200,
title = "Lua", background="blue",
border = true
}

function Window (options)
-- check mandatory options
if type(options.title) ~= "string" then
error("no title")
elseif type(options.width) ~= "number" then
error("no width")
elseif type(options.height) ~= "number" then
error("no height")
end

-- everything else is optional
_Window(options.title,
options.x or 0, -- default value
options.y or 0, -- default value
options.width, options.height,
options.background or "white", -- default
options.border -- default is false (nil)
)

第6章 再论函数

Lua 中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)

第一类值指:在Lua 中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。

词法定界指:被嵌套的函数可以访问他外部函数中的变量。这一特性给Lua提供了强大的编程能力。

Lua中关于函数稍微难以理解的是函数也可以没有名字,匿名的。当我们提到函数名(比如 print),实际上是说一个指向函数的变量,像持有其他类型值的变量一样:

既然函数是值,那么表达式也可以创建函数了,Lua中我们经常这样写:

1
function foo (x) return 2*x end

这实际上是利用Lua 提供的“语法上的甜头”(syntacticsugar)的结果,下面是原本的函数:

1
foo = function (x) return 2*x end

函数定义实际上是一个赋值语句,将类型为 function 的变量赋给一个变量。我们使用```lua

1
function(x)..end

来定义一个函数和使用{}创建一个表一样。

table 标准库提供一个排序函数,接受一个表作为输入参数并且排序表中的元素。这个函数必须能够对不同类型的值(字符串或者数值)按升序或者降序进行排序。Lua不是尽可能多地提供参数来满足这些情况的需要,而是接受一个排序函数作为参数(类似C++的函数对象),排序函数接受两个排序元素作为输入参数,并且返回两者的大小关系,例如:

1
2
3
4
5
6
network = { 
{name = "grauna", IP = "210.26.30.34"},
{name = "arraial", IP = "210.26.30.23"},
{name = "lua", IP = "210.26.23.12"},
{name = "derain", IP = "210.26.23.20"},
}

如果我们想通过表的 name 域排序:

1
2
3
table.sort(network, function (a,b) 
return (a.name > b.name)
end)

以其他函数作为参数的函数在Lua 中被称作高级函数,高级函数在Lua 中并没有特权,只是Lua把函数当作第一类函数处理的一个简单的结果。

下面给出一个绘图函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function eraseTerminal() 
io.write("\27[2J")
end
-- writes an `*' at column `x' , row `y'
function mark (x,y)
io.write(string.format("\27[%d;%dH*", y, x))
end
-- Terminal size
TermSize = {w = 80, h = 24}
-- plot a function -- (assume that domain and image are in the range [-1,1])
function plot (f)
eraseTerminal()
for i=1,TermSize.w do
local x = (i/TermSize.w)*2 - 1
local y = (f(x) + 1)/2 * TermSize.h
mark(i, y)
end
io.read() -- wait before spoiling the screen
end

要想让这个例子正确的运行,你必须调整你的终端类型和代码中的控制符一致:

1
plot(function (x) return math.sin(x*2*math.pi) end)

将在屏幕上输出一个正弦曲线。

将第一类值函数应用在表中是Lua 实现面向对象和包机制的关键,这部分内容在后面章节介绍。

6.1闭包

当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。

下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;现在想根据学生的成绩从高到低对学生进行排序,可以这样做:

1
2
3
4
5
names = {"Peter", "Paul", "Mary"} 
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2] -- compare the grades
end)

假定创建一个函数实现此功能:

1
2
3
4
5
function sortbygrade (names, grades) 
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2] -- compare the grades
end)
end

例子中包含在 sortbygrade 函数内部的 sort 中的匿名函数可以访问 sortbygrade 的参数grades,在匿名函数内部 grades 不是全局变量也不是局部变量,我们称作外部的局部变量(externallocalvariable)或者upvalue。(upvalue 意思有些误导,然而在Lua 中他的存在有历史的根源,还有他比起 external local variable 简短)。

看下面的代码:

匿名函数使用upvalue i保存他的计数,当我们调用匿名函数的时候i已经超出了作用范围,因为创建i的函数newCounter已经返回了。然而Lua 用闭包的思想正确处理了这种情况。简单的说闭包是一个函数加上它可以正确访问的upvalues。如果我们再次调用 newCounter,将创建一个新的局部变量i,因此我们得到了一个作用在新的变量i上的新闭包。

cl、c2 是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。

技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代指闭包。

闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在Lua 的函数世界里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在GUI环境中你需要创建一系列button,但用户按下button 时回调函数被调用,可能不同的按钮被按下时需要处理的任务有点区别。具体来讲,一个十进制计算器需要10个相似的按钮,每个按钮对应一个数字,可以使用下面的函数创建他们:

这个例子中我们假定 Button 是一个用来创建新按钮的工具,label 是按钮的标签,action 是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问 upvalue digit)。digitButton 完成任务返回后,局部变量 digit超出范围,回调函数仍然可以被调用并且可以访问局部变量 digit。

闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义 sin 使其接受一个度数而不是弧度作为参数:

1
2
3
4
oldSin = math.sin 
math.sin = function (x)
return oldSin(x*math.pi/180)
end

更清楚的方式:

这样我们把原始版本放在一个局部变量内,访问 sin 的唯一方式是通过新版本的函数。

利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java 里的沙箱一样),当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io 库的 open 函数来限制程序打开的文件。

6.2非全局函数

Lua 中函数可以作为全局变量也可以作为局部变量,我们已经看到一些例子:函数作为 table 的域(大部分 Lua 标准库使用这种机制来实现的比如 io.read、math.sin)。这种情况下,必须注意函数和表语法:

  1. 表和函数放在一起

  1. 使用表构造函数
1
2
3
4
Lib = { 
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
}
  1. Lua 提供另一种语法方式
1
2
3
4
5
6
7
Lib = {} 
function Lib.foo (x,y)
return x + y
end
function Lib.goo (x,y)
return x - y
end

当我们将函数保存在一个局部变量内时,我们得到一个局部函数,也就是说局部函数像局部变量一样在一定范围内有效。这种定义在包中是非常有用的:因为Lua把chunk当作函数处理,在 chunk 内可以声明局部函数(仅仅在 chunk 内可见),词法定界保证了包内的其他函数可以调用此函数。下面是声明局部函数的两种方式:

  1. 方式一
1
2
3
4
5
6
7
8
9
local f = function (...) 
...
end

local g = function (...)
...
f() -- external local `f' is visible here
...
end
  1. 方式二
1
2
3
local function f (...) 
...
end

有一点需要注意的是在声明递归局部函数的方式:

上面这种方式导致 Lua 编译时遇到 fact(n-1)并不知道他是局部函数 fact,Lua 会去查找是否有这样的全局函数fact。为了解决这个问题我们必须在定义函数以前先声明:

这样在 fact 内部 fact(n-1)调用是一个局部函数调用,运行时 fact 就可以获取正确的值了。

但是Lua扩展了他的语法使得可以在直接递归函数定义时使用两种方式都可以。

在定义非直接递归局部函数时要先声明然后定义才可以:

6.3正确的尾调用(ProperTailCalls)

Lua 中函数的另一个有趣的特征是可以正确的处理尾调用(proper tail recursion,些书使用术语“尾递归”,虽然并未涉及到递归的概念)。

尾调用是一种类似在函数结尾的 goto 调用,当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。例如:

g 的调用是尾调用。

例子中 f调用 g 后不会再做任何事情,这种情况下当被调用函数 g 结束时程序不需要返回到调用者 f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如Lua 解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语言支持正确的尾调用。

由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调用不论n为何值不会导致栈溢出。

1
2
3
function foo (n) 
if n > 0 then return foo(n - 1) end
end

需要注意的是:必须明确什么是尾调用。

一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如:

上面这个例子中 f在调用 g 后,不得不丢弃 g 地返回值,所以不是尾调用,同样的下面几个例子也不时尾调用:

Lua 中类似 return g(..)这种格式的调用是尾调用。但是 g 和 g 的参数都可以是复杂表达式,因为Lua会在调用之前计算表达式的值。例如下面的调用是尾调用:

1
return x[i].foo(x[j] + a*b, i + j) 

可以将尾调用理解成一种 goto,在状态机的编程领域尾调用是非常有用的。状态机的应用要求函数记住每一个状态,改变状态只需要 goto(or call)一个特定的函数。我们考虑一个迷宫游戏作为例子:迷宫有很多个房间,每个房间有东西南北四个门,每一步输入一个移动的方向,如果该方向存在即到达该方向对应的房间,否则程序打印警告信息。目标是:从开始的房间到达目的房间。

这个迷宫游戏是典型的状态机,每个当前的房间是一个状态。我们可以对每个房间写一个函数实现这个迷宫游戏,我们使用尾调用从一个房间移动到另外一个房间。一个四个房间的迷宫代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function room1 () 
local move = io.read()
if move == "south" then
return room3()
elseif move == "east" then
return room2()
else
print("invalid move")
return room1() -- stay in the same room
end
end

function room2 ()
local move = io.read()
if move == "south" then
return room4()
elseif move == "west" then
return room1()
else
print("invalid move")
return room2()
end
end

function room3 ()
local move = io.read()
if move == "north" then
return room1()
elseif move == "east" then
return room4()
else
print("invalid move")
return room3()
end
end

function room4 ()
print("congratilations!")
end

我们可以调用rooml0开始这个游戏。

如果没有正确的尾调用,每次移动都要创建一个栈,多次移动后可能导致栈溢出。但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个 goto 到另外一个函数并不是传统的函数调用。

第7章迭代器与泛型for

在这一章我们讨论为范性 for 写迭代器,我们从一个简单的迭代器开始,然后我们学习如何通过利用范性for的强大之处写出更高效的迭代器。

7.1迭代器与闭包

迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。在Lua 中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。

迭代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里。闭包提供的机制可以很容易实现这个任务。记住:闭包是个内部函数,它可以访问一个或者多个外部函数的外部局部变量。每次闭包的成功调用后这些外部局部变量都保存他们的值(状态)。当然如果要创建一个闭包必须要创建其外部局部变量。所以一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。

举一个简单的例子,我们为一个 list 写一个简单的迭代器,与 ipairs(不同的是我们实现的这个迭代器返回元素的值而不是索引下标:

1
2
3
4
5
6
7
8
function list_iter (t) 
local i = 0
local n = table.getn(t)
return function ()
i = i + 1
if i <= n then return t[i] end
end
end

这个例子中 list_iter是一个工厂,每次调用他都会创建一个新的闭包(迭代器本身)。闭包保存内部局部变量(t,i,n),因此每次调用他返回 list 中的下一个元素值,当 list 中没有值时,返回 nil.我们可以在 while 语句中使用这个迭代器:

1
2
3
4
5
6
7
t = {10, 20, 30} 
iter = list_iter(t) -- creates the iterator
while true do
local element = iter() -- calls the iterator
if element == nil then break end
print(element)
end

我们设计的这个迭代器也很容易用于范性for语句

1
2
3
4
t = {10, 20, 30} 
for element in list_iter(t) do
print(element)
end

范性 for 为迭代循环处理所有的薄记(bookkeeping):首先调用迭代工厂;内部保留迭代函数,因此我们不需要 iter 变量;然后在每一个新的迭代处调用迭代器函数;当迭代器返回 nil时循环结束(后面我们将看到范性 for 能胜任更多的任务)。

下面看一个稍微高级一点的例子:我们写一个迭代器遍历一个文件内的所有匹配的单词。为了实现目的,我们需要保留两个值:当前行和在当前行的偏移量,我们使用两个外部局部变量 line、pos 保存这两个值。

迭代函数的主体部分调用了 string.find 函数,string.find 在当前行从当前位置开始查找匹配的单词,例子中匹配的单词使用模式’%w+’描述的;如果查找到一个单词,迭代函数更新当前位置 pos 为单词后的第一个位置,并且返回这个单词(string.sub 函数从 lineline 可读返回 nil 结束。

尽管迭代函数有些复杂,但使用起来是很直观的:

1
2
3
for word in allwords() do
print(word)
end

通常情况下,迭代函数都难写易用。这不是一个大问题:一般Lua 编程不需要自己定义迭代函数,而是使用语言提供的,除非确实需要自己定义。

7.2范性for的语义

前面我们看到的迭代器有一个缺点:每次调用都需要创建一个闭包,大多数情况下这种做法都没什么问题,例如在 allwords 迭代器中创建一个闭包的代价比起读整个文件来说微不足道,然而在有些情况下创建闭包的代价是不能忍受的。在这些情况下我们可以使用范性 for 本身来保存迭代的状态。

前面我们看到在循环过程中范性 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数,状态常量和控制变量.下面详细说明。

范性 for 的文法如下:

是一个或多个以逗号分割的变量名列表,是一个或多个以逗号分割的表达式列表,通常情况下 exp-list 只有一个值:迭代工厂的调用。

变量列表k,v;表达式列表 pair(t),在很多情况下变量列表也只有一个变量,比如:

1
2
3
for line in io.lines() do 
io.write(line, '\n')
end

我们称变量列表中第一个变量为控制变量,其值为 nil 时循环结束。

下面我们看看范性 for的执行过程:

首先,初始化,计算in 后面表达式的值,表达式应该返回范性 for 需要的三个值:迭代函数,状态常量和控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil补足,多出部分会被忽略。

第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。

第三,将迭代函数返回的值赋给变量列表。

第四,如果返回的第一个值为 nil循环结束,否则执行循环体。

第五,回到第二步再次调用迭代函数。

更精确的来说:

如果我们的迭代函数是f,状态常量是 s,控制变量的初始值是 a0,那么控制变量将循环:al=f(s,a0)、a2=f(s,al)、·..·,直到 ai=nil。

7.3无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调型的简单的例子是ipairs,他遍历数组的每一个元素。

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs 和迭代函数都很简单,我们在Lua 中可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
function iter (a, i) 
i = i + 1
local v = a[i]
if v then
return i, v
end
end

function ipairs (a)
return iter, a, 0
end

当 Lua 调用 ipairs(a)开始循环时,他获取三个值:迭代函数 iter,状态常量 a 和控制变)非)返回 2,a[2]···直到第一个非nil 元素。

Lua 库中实现的 pairs 是一个用 next实现的原始方法:

还可以不使用ipairs直接使用next

1
2
3
for k, v in next, t do
...
end

记住:exp-list 返回结果会被调整为三个,所以 Lua 获取 next、t、nil;确切地说当他调用 pairs 时获取。

7.4多状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

下面我们重写allwords迭代器,这一次我们不是使用闭包而是使用带有两个域(line,pos)的 table 。

开始迭代的函数是很简单的,他必须返回迭代函数和初始状态:

真正的处理工作是在迭代函数内完成:

我们应该尽可能的写无状态的迭代器,因为这样循环的时候由 for 来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包;尽可能不要使用 table 这种方式,因为创建闭包的代价要比创建 table 小,另外Lua 处理闭包要比处理table 速度快些。后面我们还将看到另一种使用协同来创建迭代器的方式,这种方式功能更强但更复杂。

7.5真正的迭代器

迭代器的名字有一些误导,因为它并没有迭代,完成迭代功能的是 for 语句,也许更好的叫法应该是’生成器;但是在其他语言比如java、C++迭代器的说法已经很普遍了,我们也将沿用这种术语。

有一种方式创建一个在内部完成迭代的迭代器。这样当我们使用迭代器的时候就不需要使用循环了;我们仅仅使用每一次迭代需要处理的任务作为参数调用迭代器即可,具体地说,迭代器接受一个函数作为参数,并且这个函数在迭代器内部被调用。

作为一个具体的例子,我们使用上述方式重写 allwords 迭代器:

1
2
3
4
5
6
7
8
9
10
function allwords (f) 
-- repeat for each line in the file
for l in io.lines() do
-- repeat for each word in the line
for w in string.gfind(l, "%w+") do
-- call the function
f(w)
end
end
end

如果我们想要打印出单词,只需要

更一般的做法是我们使用匿名函数作为作为参数,下面的例子打印出单词hello’出现的次数:

1
2
3
4
5
local count = 0 
allwords(function (w)
if w == "hello" then count = count + 1 end
en )
print(count)

用 for 结构完成同样的任务:

1
2
3
4
5
local count = 0 
for w in allwords() do
if w == "hello" then count = count + 1 end
end
print(count)

真正的迭代器风格的写法在Lua 老版本中很流行,那时还没有for 循环。

两种风格的写法相差不大,但也有区别:一方面,第二种风格更容易书写和理解;另一方面,for 结构更灵活,可以使用 break和 continue 语句;在真正的迭代器风格写法中return语句只是从匿名函数中返回而不是退出循环。

第8章编译·运行·调试

虽然我们把Lua 当作解释型语言,但是Lua 会首先把代码预编译成中间码然后再执行(很多解释型语言都是这么做的)。在解释型语言中存在编译阶段听起来不合适,然而,解释型语言的特征不在于他们是否被编译,而是编译器是语言运行时的一部分,所以,执行编译产生的中间码速度会更快。我们可以说函数 dofile 的存在就是说明可以将Lua作为一种解释型语言被调用。

前面我们介绍过 dofile,把它当作Lua 运行代码的 chunk 的一种原始的操作。dofile实际上是一个辅助的函数。真正完成功能的函数是 loadfile;与 dofile 不同的是 loadfile不会抛出错误信息而是返回错误代。.我们可以这样定义dofile:

如果loadfile失败assert会抛出错误。

完成简单的功能 dofile 比较方便,他读入文件编译并且执行。然而loadfile更加灵活。在发生错误的情况下,loadfile 返回 nil 和错误信息,这样我们就可以自定义错误处理。另外,如果我们运行一个文件多次的话,loadfile 只需要编译一次,但可多次运行。dofile却每次都要编译。

loadstring与loadfile 相似,只不过它不是从文件里读入chunk,而是从一个串中读入。例如:

f将是一个函数,调用时执行i=i+l。

1
2
3
i = 0
f(); print(i) --> 1
f(); print(i) --> 2

loadstring 函数功能强大,但使用时需多加小心。确认没有其它简单的解决问题的方法再使用。

Lua 把每一个 chunk 都作为一个匿名函数处理。例如:chunk”a=1”,loadstring 返回与其等价的 function(a=1 end

与其他函数一样,chunks 可以定义局部变量也可以返回值:

1
2
f = loadstring("local a = 10; return a + 20")
print(f()) --> 30

loadfile 和loadstring都不会抛出错误,如果发生错误他们将返回 nil加上错误信息:

另外,loadfile 和loadstring 都不会有边界效应产生,他们仅仅编译 chunk成为自己内部实现的一个匿名函数。通常对他们的误解是他们定义了函数。Lua中的函数定义是发生在运行时的赋值而不是发生在编译时。假如我们有一个文件foo.lua:

1
2
3
4
-- file `foo.lua' 
function foo (x)
print(x)
end

当我们执行命令f =loadfile(“foo.lua”)后,foo 被编译了但还没有被定义,如果要定义他必须运行chunk:

1
2
f()    -- defines `foo' 
foo("ok") --> ok

如果你想快捷的调用 dostring(比如加载并运行),可以这样

1
loadstring(s)()

调用 loadstring 返回的结果,然而如果加载的内容存在语法错误的话,loadstring 返

1
assert(loadstring(s))()

通常使用loadstring 加载一个字串没什么意义,例如:

1
f = loadstring("i = i + l")

大概与f=function(i=i+1 end 等价,但是第二段代码速度更快因为它只需要编译一次,第一段代码每次调用loadstring 都会重新编译,还有一个重要区别:loadstring 编译的时候不关心词法范围:

1
2
3
local i = 0 
f = loadstring("i = i + 1")
g = function () i = i + 1 end

这个例子中,和想象的一样 g使用局部变量i,然而f使用全局变量i;loadstring 总是在全局环境中编译他的串。

loadstring通常用于运行程序外部的代码,比如运行用户自定义的代码。注意:loadstring 期望一个 chunk,即语句。如果想要加载表达式,需要在表达式前加 return,那样将返回表达式的值。看例子:

1
2
3
4
print "enter your expression:" 
local l = io.read()
local func = assert(loadstring("return " .. l))
print("the value of your expression is " .. func())

loadstring返回的函数和普通函数一样,可以多次被调用:

1
2
3
4
5
6
7
print "enter function to be plotted (with variable `x'):" 
local l = io.read()
local f = assert(loadstring("return " .. l))
for i=1,20 do
x = i -- global `x' (to be visible from the chunk)
print(string.rep("*", f()))
end

8.1reguire函数

Lua 提供高级的 require 函数来加载运行库。粗略的说 require 和 dofile 完成同样的功能但有两点不同:

  1. require 会搜索目录加载文件

  2. require 会判断是否文件已经加载避免重复加载同一文件。由于上述特征,require在Lua中是加载库的更好的函数。

require 使用的路径和普通我们看到的路径还有些区别,我们一般见到的路径都是-个目录列表。require 的路径是一个模式列表,每一个模式指明一种由虚文件名(require的参数)转成实文件名的方法。更明确地说,每一个模式是一个包含可选的问号的文件名。匹配的时候Lua会首先将问号用虚文件名替换,然后看是否有这样的文件存在。如果不存在继续用同样的方法用第二个模式匹配。例如,路径如下:

1
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

调用 require”lili”时会试着打开这些文件:

1
2
3
4
lili 
lili.lua
c:\windows\lili
/usr/local/lua/lili/lili.lua

require 关注的问题只有分号(模式之间的分隔符)和问号,其他的信息(目录分隔符,文件扩展名)在路径中定义。

为了确定路径,Lua 首先检查全局变量LUA_PATH是否为一个字符串,如果是则认为这个串就是路径;否则 require 检查环境变量 LUA_PATH 的值,如果两个都失败 require使用固定的路径(典型的”?;?.lua”)

require 的另一个功能是避免重复加载同一个文件两次。Lua 保留一张所有已经加载的文件的列表(使用 table 保存)。如果一个加载的文件在表中存在 require 简单的返回;表中保留加载的文件的虚名,而不是实文件名。所以如果你使用不同的虚文件名 require同一个文件两次,将会加载两次该文件。比如 require “foo”和 require“”foo.lua”,路径为这样我们就可以判断文件是否被加载过;同样我们也可以使用一点小技巧让require 加载一个文件两次。比如,require “foo”之后_LOADED[“foo”]将不为 nil,我们可以将其赋值为nil,require”foo.lua”将会再次加载该文件。

个路径中的模式也可以不包含问号而只是一个固定的路径,比如:

1
?;?.lua;/usr/local/default.lua

这种情况下,require没有匹配的时候就会使用这个固定的文件(当然这个固定的路径必须放在模式列表的最后才有意义)。在require运行一个chunk以前,它定义了一个全局变量_REQUIREDNAME 用来保存被 required 的虚文件的文件名。我们可以通过使用这个技巧扩展 require 的功能。举个极端的例子,我们可以把路径设为
“/usr/local/lua/newrequire.lua”,这样以后每次调用 require 都会运行 newrequire.lua,这种情况下可以通过使用_REQUIREDNAME的值去实际加载required的文件。

8.2 C Packages

Lua 和C是很容易结合的,使用C为Lua 写包。与Lua 中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制,然而动态连接库不是ANSIC的一部分,也就是说在标准C中实现动态连接是很困难的。

通常Lua不包含任何不能用标准C实现的机制,动态连接库是一个特例。我们可以将动态连接库机制视为其他机制之母:一旦我们拥有了动态连接机制,我们就可以动态的加载Lua 中不存在的机制。所以,在这种特殊情况下,Lua 打破了他平台兼容的原则FreeBSD、Solaris 和其他一些Unix平台实现了这种机制,扩展其它平台支持这种机制也是不难的。在 Lua 提示符下运行 print(loadibO)看返回的结果,如果显示 bad arguments

Lua在一个叫loadlib 的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:

1
2
local path = "/usr/local/lua/lib/libluasocket.so" 
local f = loadlib(path, "luaopen_socket")

loadlib 函数加载指定的库并且连接到Lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为Lua 的一个函数,这样我们就可以直接在Lua中调用他。如果加载动态库或者查找初始化函数时出错,loadlib 将返回 nil和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:

1
2
3
local path = "/usr/local/lua/lib/libluasocket.so" -- or path = "C:\\windows\\luasocket.dll" 
local f = assert(loadlib(path, "luaopen_socket"))
f() -- actually open the library

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装二进制库的时候可以随便放在某个目录,只需要修改 stub 文件对应二进制库的实际路径即可。将 stub 文件所在的目录加入到LUA_PATH,这样设定后就可以使用 require 函数加载C库了。

8.3错误

Errare humanum ést(拉丁谚语:犯错是人的本性)。所以我们要尽可能的防止错误的发生,Lua 经常作为扩展语言嵌入在别的应用中,所以不能当错误发生时简单的崩溃或者退出。相反,当错误发生时Lua 结束当前的chunk并返回到应用中。

当Lua遇到不期望的情况时就会抛出错误,比如:两个非数字进行相加;调用一个非函数的变量;访问表中不存在的值等(可以通过 metatables 修改这种行为,后面介绍)。你也可以通过调用error 函数显示的抛出错误,error 的参数是要抛出的错误信息。

1
2
3
print "enter a number:" 
n = io.read("*number")
if not n then error("invalid input") end

Lua提供了专门的内置函数 assert来完成上面类似的功能:

1
2
print "enter a number:" 
n = assert(io.read("*number"), "invalid input")

assert 首先检查第一个参数是否返回错误,如果不返回错误 assert 简单的返回,否则assert 以第二个参数抛出错误信息。第二个参数是可选的。注意 assert是普通的函数,他会首先计算两个参数然后再调用函数,所以以下代码:

1
2
n = io.read() 
assert(tonumber(n), "invalid input: " .. n .. " is not a number")

将会总是进行连接操作,使用显示的test可以避免这种情况。

当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误。这两种方式选择哪一种没有固定的规则,但有一般的原则:容易避免的异常应该抛出错误否则返回错误代码。

例如我们考虑 sin 函数,如果以一个 table 作为参数,假定我们返回错误代码,我们需要检查错误的发生,代码可能如下:

1
2
3
local res = math.sin(x)
if not res then -- error
...

local res = math.sin(x)

然而我们可以在调用函数以前很容易的判断是否有异常:

然而通常情况下我们既不是检查参数也不是检查返回结果,因为参数错误可能意味着我们的程序某个地方存在问题,这种情况下,处理异常最简单最实际的方式是抛出错误并且终止代码的运行。

再来看一个例子io.open 函数用来打开一个文件,如果文件不存在结果会怎么样呢?很多系统中,通过试着去打开文件来判断是否文件存在。所以如果io.open 不能打开文件(由于文件不存在或者没有权限),函数返回 nil和错误信息。以这种方式我们可以通过与用户交互(比如:是否要打开另一个文件)合理的处理问题:

1
2
3
4
5
6
7
8
local file, msg 
repeat
print "enter a file name:"
local name = io.read()
if not name then return end -- no input
file, msg = io.open(name, "r")
if not file then print(msg) end
until file

如果你想偷懒不想处理这些情况,又想代码安全的运行,可以简单的使用 assert:

1
file = assert(io.open(name, "r")) 

Lua 中有一个习惯:如果io.open 失败,assert将抛出错误。

注意:io.open 返回的第二个结果(错误信息)作为 assert 的第二个参数。

8.4异常和错误处理

很多应用中,不需要在Lua 进行错误处理,一般有应用来完成。通常应用要求Lua运行一段 chunk,如果发生异常,应用根据Lua 返回的错误代码进行处理。在控制台模式下的Lua解释器如果遇到异常,打印出错误然后继续显示提示符等待下一个命令。

如果在Lua 中需要处理错误,需要使用 pcall 函数封装你的代码。

假定你想运行一段Lua代码,这段代码运行过程中可以捕捉所有的异常和错误。

第一步:将这段代码封装在一个函数内

1
2
3
4
5
6
7
function foo () 
...
if unexpected_condition then error() end
...
print(a[i]) -- potential error: `a' may not be a table
...
end

第二步:使用 pcall 调用这个函数

1
2
3
4
5
6
7
if pcall(foo) then 
-- no errors while running `foo'
...
else
-- `foo' raised an error: take appropriate actions
...
end

当然也可以用匿名函数的方式调用 pcall:

pcall在保护模式下调用他的第一个参数并运行,因此可以捕获所有的异常和错误。如果没有异常和错误,pcall 返回 true 和调用返回的任何值;否则返回 nil 加错误信息。

错误信息不一定非要是一个字符串(下面的例子是一个 table),传递给 error 的任何信息都会被 pcall 返回:

1
2
local status, err = pcall(function () error({code=121}) end) 
print(err.code) --> 121

这种机制提供了我们在Lua 中处理异常和错误的所需要的全部内容。我们通过 error抛出异常,然后通过 pcall捕获他。

8.5错误信息和回跟踪(Tracebacks)

虽然你可以使用任何类型的值作为错误信息,通常情况下,我们使用字符串来描述遇到的错误信息。如果遇到内部错误(比如对一个非table 的值使用索引下表访问)Lua将自己产生错误信息,否则Lua 使用传递给error 函数的参数作为错误信息。不管在什么情况下,Lua都尽可能清楚的描述发生的错误。

1
2
3
4
5
local status, err = pcall(function () a = 'a'+1 end) 
print(err) --> stdin:1: attempt to perform arithmetic on a string value
local status, err = pcall(function () error("my error") end)
print(err)
--> stdin:1: my error

例子中错误信息给出了文件名(stdin)加上行号。

函数error还可以有第二个参数,表示错误的运行级别。有了这个参数你就无法抵赖错误是别人的了,比如,加入你写了一个函数用来检查error 是否被正确的调用:

1
2
3
4
5
6
function foo (str) 
if type(str) ~= "string" then
error("string expected")
end
...
end

可能有人这样调用这个函数:

1
foo({x=1}) 

Lua 会指出发生错误的是foo 而不是 error,实际的错误是调用 error 时产生的,为了纠正这个问题修改前面的代码让 error报告错误发生在第二级 (你自己的函数是第一级)如下:

当错误发生的时候,我们常常需要更多的错误发生相关的信息,而不单单是错误发生的位置。至少期望有一个完整的显示导致错误发生的调用栈的 tracebacks,当 pcall 返回错误信息的时候他已经释放了保存错误发生情况的栈的信息。因此,如果我们想得到tracebacks 我们必须在 pcall 返回以前获取。Lua 提供了xpcall 来实现这个功能,xpcall误处理函数,因此可以使用 debug 库收集错误相关的信息。有两个常用的 debug 处理函数:debug。debug 和 debug.traceback,前者给出Lua 的提示符,你可以自己动手察看错误发生时的情况;后者通过 traceback 创建更多的错误信息,后者是控制台解释器用来构建错误信息的函数。你可以在任何时候调用 debug.traceback获取当前运行的traceback信息:

1
print(debug.traceback())

第9章 协同程序

协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针,但是和其他协同程序共享全局变量等很多信息。线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同是非常强大的功能,但是用起来也很复杂。如果你第一次阅读本章时不理解本章中的例子请不要担心,你可以继续阅读本书的其他部分然后再回过头来阅读本章。

9.1协同的基础

Lua 通过 table 提供了所有的协同函数,create 函数创建一个新的协同程序,create只有一个参数:协同程序将要运行的代码封装而成的函数,返回值为 thread类型的值表示创建了一个新的协同程序。通常情况下,create 的参数是一个匿名函数:

协同有三个状态:挂起态、运行态、停止态。当我们创建一个协同程序时他开始的状态为挂起态,也就是说我们创建协同程序的时候不会自动运行,可以使用 status 函数检查协同的状态:

函数coroutine.resume 可以使程序由挂起状态变为运行态:

这个例子中,协同体仅仅打印出”hi”之后便进入终止状态:

当目前为止,协同看起来只是一种复杂的调用函数的方式,真正的强大之处体现在yield 函数,它可以将正在运行的代码挂起,看一个例子:

1
2
3
4
5
6
co = coroutine.create(function () 
for i=1,10 do
print("co", i)
coroutine.yield()
end
end)

现在重新执行这个协同程序,程序将在第一个yield处被挂起:

从协同的观点看:使用函数yield可以使程序挂起,当我们激活被挂起的程序时,yield
返回并继续程序的执行直到再次遇到yield或者程序结束。

上面最后一次调用的时候,协同体已经结束,因此协同程序处于终止状态。如果我们仍然企图激活他,resume 将返回 false和错误信息。

1
2
print(coroutine.resume(co)) 
--> false cannot resume dead coroutine

注意:resume 运行在保护模式下,因此如果协同内部存在错误Lua 并不会抛出错误而是将错误返回给resume函数。

Lua 中一对 resume-yield 可以相互交换数据。

下面第一个例子resume,没有相应的 yield,resume 把额外的参数传递给协同的主程序。

1
2
3
4
co = coroutine.create(function (a,b,c) 
print("co", a,b,c)
end)
coroutine.resume(co, 1, 2, 3) --> co 1 2 3

第二个例子,resume 返回除了 true 以外的其他部分将作为参数传递给相应的 yield

1
2
3
4
co = coroutine.create(function (a,b) 
coroutine.yield(a + b, a - b)
end)
print(coroutine.resume(co, 20, 10)) --> true 30 10

对称性,yield返回的额外的参数也将会传递给 resume。

1
2
3
4
5
co = coroutine.create (function ()
print("co", coroutine.yield())
end)
coroutine.resume(co)
coroutine.resume(co, 4, 5) --> co 4 5

最后一个例子,当协同代码结束时主函数返回的值都会传给相应的resume:

我们很少在同一个协同程序中使用这几种特性,但每一种都有其用处

现在已经了解了一些协同的内容,在我们继续学习以前,先要澄清两个概念:Lua提供的这种协同我们称为不对称的协同,就是说挂起一个正在执行的协同的函数与使一个被挂起的协同再次执行的函数是不同的,有些语言提供对称的协同,这种情况下,由执行到挂起之间状态转换的函数是相同的。

有人称不对称的协同为半协同,另一些人使用同样的术语表示真正的协同,严格意义上的协同不论在什么地方只要它不是在其他的辅助代码内部的时候都可以并且只能使执行挂起,不论什么时候在其控制栈内都不会有不可决定的调用。(However,other peopleuse the same term semi-coroutine to denote a restricted implementation of coroutines, where acoroutine can only suspend its execution when it is not inside any auxiliary function, that is,when it has no pending calls in its control stack.)。只有半协同程序的主体中才可以 yield,python 中的产生器(generator)就是这种类型的半协同的例子。

与对称的协同和不对称协同的区别不同的是,协同与产生器的区别更大。产生器相对比较简单,他不能完成真正的协同所能完成的一些任务。我们熟练使用不对称的协同之后,可以利用不对称的协同实现比较优越的对称协同。

9.2管道和过滤器

协同最有代表性的作用是用来描述生产者-消费者问题。我们假定有一个函数在不断(中一)一中)这两个函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function producer () 
while true do
local x = io.read()
send(x) -- produce new value -- send to consumer
end
end

function consumer ()
while true do
local x = receive() -- receive from producer
io.write(x, "\n") -- consume new value
end
end

(例子中生产者和消费者都在不停的循环,修改一下使得没有数据的时候他们停下
来并不困难),问题在于如何使得receive和send协同工作。只是一个典型的谁拥有住循
环的情况,生产者和消费者都处在活动状态,都有自己的主循环,都认为另一方是可调
用的服务。对于这种特殊的情况,可以改变一个函数的结构解除循环,使其作为被动的
接受。然而这种改变在某些特定的实际情况下可能并不简单。

协同为解决这种问题提供了理想的方法,因为调用者与被调用者之间的resume-yield关系会不断颠倒。当一个协同调用 yield时并不会进入一个新的函数,取而代之的是返回一个未决的 resume 的调用。相似的,调用 resume 时也不会开始一个新的函数而是返回yield 的调用。这种性质正是我们所需要的,与使得 send-receive 协同工作的方式是一致的.receive 唤醒生产者生产新值,send把产生的值送给消费者消费。

这种设计下,开始时调用消费者,当消费者需要值时他唤起生产者生产值,生产者生产值后停止直到消费者再次请求。我们称这种设计为消费者驱动的设计。

我们可以使用过滤器扩展这个涉及,过滤器指在生产者与消费者之间,可以对数据进行某些转换处理。过滤器在同一时间既是生产者又是消费者,他请求生产者生产值并且转换格式后传给消费者,我们修改上面的代码加入过滤器(每一行前面加上行号)。完整的代码如下:

可以调用:

1
2
3
p = producer()
f = filter(p)
consumer(f)

或者:

看完上面这个例子你可能很自然的想到UNIX的管道,协同是一种非抢占式的多线程。管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在独立的协同代码中。管道在读(consumer)与写(producer)之间提供了一个缓冲,因此两者相关的的速度没有什么限制,在上下文管道中这是非常重要的,因为在进程间的切换代价是很高的。协同模式下,任务间的切换代价较小,与函数调用相当,因此读写可以很好的协同处理。

9.3用作迭代器的协同

我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给循环体消费。所以可以使用协同来实现迭代器。协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态。

我们来完成一个打印一个数组元素的所有的排列来阐明这种应用。直接写这样一个迭代函数来完成这个任务并不容易,但是写一个生成所有排列的递归函数并不难。思路是这样的:将数组中的每一个元素放到最后,依次递归生成所有剩余元素的排列。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function permgen (a, n) 
if n == 0 then
printResult(a)
else
for i=1,n do

-- put i-th element as the last one
a[n], a[i] = a[i], a[n]

-- generate all permutations of the other elements
permgen(a, n - 1)

-- restore i-th element
a[n], a[i] = a[i], a[n]

end
end
end

function printResult (a)
for i,v in ipairs(a) do
io.write(v, " ")
end
io.write("\n")
end

permgen ({1,2,3,4}, 4)

有了上面的生成器后,下面我们将这个例子修改一下使其转换成一个迭代函数:

  1. 第一步 printResult 改为 yield

  1. 第二步,我们定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使生成器运行在一个协同程序内。迭代函数负责请求协同产生下一个可能的排列。

这样我们就可以使用 for循环来打印出一个数组的所有排列情况了:

perm 函数使用了 Lua 中常用的模式:将一个对协同的 resume 的调用封装在一个函数内部,这种方式在Lua非常常见,所以Lua专门为此专门提供了一个函数coroutine.wrap。与 create 相同的是,wrap 创建一个协同程序;不同的是 wrap 不返回协同本身,而是返回一个函数,当这个函数被调用时将 resume 协同。wrap 中 resume 协同以使用 wrap 重写 perm:

1
2
3
4
function perm (a) 
local n = table.getn(a)
return coroutine.wrap(function () permgen(a, n) end)
end

一般情况下,coroutine.wrap比 coroutine.create使用起来简单直观,前者更确切的提供了我们所需要的:一个可以 resume 协同的函数,然而缺少灵活性,没有办法知道 wrap所创建的协同的状态,也没有办法检查错误的发生。

9.4非抢占式多线程

如前面所见,Lua中的协同是一协作的多线程,每一个协同等同于一个线程,yield-resume 可以实现在线程中切换。然而与真正的多线程不同的是,协同是非抢占式的。当一个协同正在运行时,不能在外部终止他。只能通过显示的调用 yield 挂起他的执行。对于某些应用来说这个不存在问题,但有些应用对此是不能忍受的。不存在抢占式调用的程序是容易编写的。不需要考虑同步带来的bugs,因为程序中的所有线程间的同步都是显示的。你仅仅需要在协同代码超出临界区时调用 yield 即可。

对非抢占式多线程来说,不管什么时候只要有一个线程调用一个阻塞操作(blocking)忍受的,这使得很多程序员离协同而去。下面我们将看到这个问题可以被有趣的解决。

看一个多线程的例子:我们想通过http 协议从远程主机上下在一些文件。我们使用Diego Nehab 开发的LuaSocket 库来完成。我们先看下在一个文件的实现,大概步骤是打开一个到远程主机的连接,发送下载文件的请求,开始下载文件,下载完毕后关闭连接。

第一,加载LuaSocket库

第二,定义远程主机和需要下载的文件名

1
2
host = "www.w3.org" 
file = "/TR/REC-html32.html"

第三,打开一个TCP 连接到远程主机的 80 端口(http 服务的标准端口)

1
c = assert(socket.connect(host, 80)) 

上面这句返回一个连接对象,我们可以使用这个连接对象请求发送文件

1
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n") 

receive 函数返回他送接收到的数据加上一个表示操作状态的字符串。当主机断开连接时,我们退出循环。

第四,关闭连接

1
c:close()

现在我们知道了如何下载一个文件,下面我们来看看如何下载多个文件。一种方法是我们在一个时刻只下载一个文件,这种顺序下载的方式必须等前一个文件下载完成后一个文件才能开始下载。实际上是,当我们发送一个请求之后有很多时间是在等待数据的到达,也就是说大部分时间浪费在调用receive上。如果同时可以下载多个文件,效率将会有很大提高。当一个连接没有数据到达时,可以从另一个连接读取数据。很显然,协同为这种同时下载提供了很方便的支持,我们为每一个下载任务创建一个线程,当一个线程没有数据到达时,他将控制权交给一个分配器,由分配器唤起另外的线程读取数据。

使用协同机制重写上面的代码,在一个函数内:

由于我们不关心文件的内容,上面的代码只是计算文件的大小而不是将文件内容输出。(当有多个线程下载多个文件时,输出会混杂在一起),在新的函数代码中,我们使用receive从远程连接接收数据,在顺序接收数据的方式下代码如下:

1
2
3
function receive (connection) 
return connection:receive(2^10)
end

在同步接受数据的方式下,函数接收数据时不能被阻塞,而是在没有数据可取时yield,代码如下:

调用函数 timeout(O)使得对连接的任何操作都不会阻塞。当操作返回的状态为timeout 时意味着操作未完成就返回了。在这种情况下,线程 yield。非 false 的数值作为yield的参数告诉分配器线程仍在执行它的任务。(后面我们将看到分配器需要timeout连接的情况),注意:即使在 timeout 模式下,连接依然返回他接受到直到 timeout 为止,因此receive会一直返回 s给她的调用者。

下面的函数保证每一个下载运行在自己独立的线程内:

代码中 table 中为分配器保存了所有活动的线程。

分配器代码是很简单的,它是一个循环,逐个调用每一个线程。并且从线程列表中移除已经完成任务的线程。当没有线程可以运行时退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
function dispatcher () 
while true do
local n = table.getn(threads)
if n == 0 then break end -- no more threads to run
for i=1,n do
local status, res = coroutine.resume(threads[i])
if not res then -- thread finished its task?
table.remove(threads, i)
break
end
end
end
end

最后,在主程序中创建需要的线程调用分配器,例如:从 W3C 站点上下载4个文件:

使用协同方式下,我的机器花了6s下载完这几个文件;顺序方式下用了15s,大概2 倍的时间。

尽管效率提高了,但距离理想的实现还相差甚远,当至少有一个线程有数据可读取的时候,这段代码可以很好的运行。否则,分配器将进入忙等待状态,从一个线程到另个线程不停的循环判断是否有数据可获取。结果协同实现的代码比顺序读取将花费30倍的CPU时间。

为了避免这种情况出现,我们可以使用LuaSocket 库中的 select 函数。当程序在一组 socket中不断的循环等待状态改变时,它可以使程序被阻塞。我们只需要修改分配器,使用 select 函数修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function dispatcher () 
while true do
local n = table.getn(threads)
if n == 0 then break end -- no more threads to run
local connections = {}
for i=1,n do
local status, res = coroutine.resume(threads[i])
if not res then -- thread finished its task?
table.remove(threads, i)
break
else -- timeout
table.insert(connections, res)
end
end
if table.getn(connections) == n then
socket.select(connections)
end
end
end

在内层的循环分配器收集连接表中timeout地连接,注意:receive将连接传递给yield,因此 resume 返回他们。当所有的连接都 timeout 分配器调用 select 等待任一连接状态的改变。最终的实现效率和上一个协同实现的方式相当,另外,他不会发生忙等待,比起顺序实现的方式消耗CPU的时间仅仅多一点点。

第10章完整示例

我们看两个完整的例子来阐明Lua 语言的使用。第一个例子来自于Lua 网站,他展示了Lua作为数据描述语言的使用。第二个例子讲解了马尔可夫链算法的实现,这个算法在Kernighan & Pike 著作的 Practice of Programming 书中也有描述。这两个完整的例子之后,Lua 语言方面的介绍便到此结束。后面将继续介绍table 和面向对象的内容以及标准库、C-API等。

10.1Lua作为数据描述语言使用

Lua 网站保留一个包含世界各地使用Lua 创建的工程的例子的数据库。在数据库中我们用一个构造器以自动归档的方式表示每一个工程入口点,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
entry{ 
title = "Tecgraf",
org = "Computer Graphics Technology Group, PUC-Rio",
url = "http://www.tecgraf.puc-rio.br/",
contact = "Waldemar Celes",
description = [[
TeCGraf is the result of a partnership between PUC-Rio,
the Pontifical Catholic University of Rio de Janeiro,
and <A HREF="http://www.petrobras.com.br/">PETROBRAS</A>,
the Brazilian Oil Company.
TeCGraf is Lua's birthplace,
and the language has been used there since 1993.
Currently, more than thirty programmers in TeCGraf use
Lua regularly; they have written more than two hundred
}
thousand lines of code, distributed among dozens of
final products.]]
}

有趣的是,工程入口的列表是存放在一个Lua 文件中的,每个工程入口以 table 的形式作为参数去调用 entry 函数。我们的目的是写一个程序将这些数据以 html 格式展示出来。由于工程太多,我们首先列出工程的标题,然后显示每个工程的明细。结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<HTML> 
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.


<UL>
<LI><A HREF="#1">TeCGraf</A>
<LI> ...
</UL>

<H3>
<A NAME="1"
HREF="http://www.tecgraf.puc-rio.br/">TeCGraf</A>


<SMALL><EM>Computer Graphics Technology Group,
PUC-Rio</EM></SMALL>
</H3>

TeCGraf is the result of a partnership between
...
distributed among dozens of final products.<P>
Contact: Waldemar Celes

<A NAME="2"></A><HR>
...

</BODY></HTML>

为了读取数据,我们需要做的是正确的定义函数 entry,然后使用 dofile 直接运行数据文件即可(db.lua)。注意,我们需要遍历入口列表两次,第一次为了获取标题,第二所有的入口放在一个数组内;另一种方法:使用不同的entry 函数运行数据文件两次。因为Lua 编译文件是很快的,这里我们选用第二种方法。

首先,我们定义一个辅助函数用来格式化文本的输出(参见5.2函数部分内容)

1
2
3
function fwrite (fmt, ...) 
return io.write(string.format(fmt, unpack(arg)))
end

第二,我们定义一个BEGIN 函数用来写html 页面的头部

1
2
3
4
5
6
7
8
9
function BEGIN() 
io.write([[
<HTML>
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.
]])
end

第三,定义 entry 函数

a.第一个entry函数,将每个工程一列表方式写出,entry的参数o是描述工程的table。

1
2
3
4
5
function entry0 (o) 
N=N + 1
local title = o.title or '(no title)'
fwrite('<LI><A HREF="#%d">%s</A>\n', N, title)
end

如果 o.title 为 nil 表明 table 中的域 title 没有提供,我们用固定的”no title”替换。

b.第二个 éntry 函数,写出工程所有的相关信息,稍微有些复杂,因为所有项都是可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function entry1 (o) 
N=N + 1
local title = o.title or o.org or 'org'
fwrite('<HR>\n<H3>\n')
local href = ''

if o.url then
href = string.format(' HREF="%s"', o.url)
end
fwrite('<A NAME="%d"%s>%s</A>\n', N, href, title)

if o.title and o.org then
fwrite('\n<SMALL><EM>%s</EM></SMALL>', o.org)
end
fwrite('\n</H3>\n')

if o.description then
fwrite('%s', string.gsub(o.description,
'\n\n\n*', '<P>\n'))
fwrite('<P>\n')
end

if o.email then
fwrite('Contact: <A HREF="mailto:%s">%s</A>\n',
o.email, o.contact or o.email)
elseif o.contact then
fwrite('Contact: %s\n', o.contact)
end
end

由于html中使用双引号,为了避免冲突我们这里使用单引号表示串。

第四,定义END 函数,写 html 的尾部

1
2
3
function END() 
fwrite('</BODY></HTML>\n')
end

在主程序中,我们首先使用第一个 entry 运行数据文件输出工程名称的列表,然后再以第二个 entry 运行数据文件输出工程相关信息。

10.2马尔可夫链算法

我们第二个例子是马尔可夫链算法的实现,我们的程序以前 n(n=2)个单词串为基础随机产生一个文本串。

程序的第一部分读出原文,并且对没两个单词的前缀建立一个表,这个表给出了具有那些前缀的单词的一个顺序。建表完成后,这个程序利用这张表生成一个随机的文本。在此文本中,每个单词都跟随着它的的前两个单词,这两个单词在文本中有相同的概率。这样,我们就产生了一个非常随机,但并不完全随机的文本。例如,当应用这个程序的输出结果会出现“构造器也可以通过表构造器,那么一下几行的插入语对于整个文件来说,不是来存储每个功能的内容,而是来展示它的结构。”如果你想在队列里找到最大元素并返回最大值,接着显示提示和运行代码。下面的单词是保留单词,不能用在度和弧度之间转换。

我们编写一个函数用来将两个单词中间加上空个连接起来:

我们用 NOWORD(即\n)表示文件的结尾并且初始化前缀单词,例如,下面的文
本:

1
the more we try the more we do

初始化构造的表为:

1
2
3
4
5
6
7
8
{ 
["\n \n"] = {"the"},
["\n the"] = {"more"},
["the more"] = {"we", "we"},
["we try"] = {"the"},
["try the"] = {"more"},
["we do"] = {"\n"},
}

我们使用全局变量 statetab 来保存这个表,下面我们完成一个插入函数用来在这个statetab中插入新的单词。

1
2
3
4
5
6
7
function insert (index, value) 
if not statetab[index] then
statetab[index] = {value}
else
table.insert(statetab[index], value)
end
end

这个函数中首先检查指定的前缀是否存在,如果不存在则创建一个新的并赋上新值。如果已经存在则调用table.insert将新值插入到列表尾部。

我们使用两个变量w1和w2 来保存最后读入的两个单词的值,对于每一个前缀,我们保存紧跟其后的单词的列表。例如上面例子中初始化构造的表。

初始化表之后,下面来看看如何生成一个MAXGEN(=1000)个单词的文本。首先,重新初始化 w1和 w2,然后对于每一个前缀,在其next 单词的列表中随机选择一个,打印此单词并更新w1和w2,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- Markov Chain Program in Lua 

function allwords ()
local line = io.read() -- current line
local pos = 1 -- current position in the line
return function () -- iterator function
while line do -- repeat while there are lines
local s, e = string.find(line, "%w+", pos)
if s then -- found a word?
pos = e + 1 -- update next position
return string.sub(line, s, e) -- return the word
else
line = io.read() -- word not found; try next line
pos = 1 -- restart from first position
end
end
return nil -- no more lines: end of traversal
end
end
function prefix (w1, w2)
return w1 .. ' ' .. w2
end

local statetab

function insert (index, value)
if not statetab[index] then
statetab[index] = {n=0}
end
table.insert(statetab[index], value)
end

local N = 2
local MAXGEN = 10000
local NOWORD = "\n"
-- build table
statetab = {}
local w1, w2 = NOWORD, NOWORD
for w in allwords() do
insert(prefix(w1, w2), w)
w1 = w2; w2 = w;
end
insert(prefix(w1, w2), NOWORD)
-- generate text
w1 = NOWORD; w2 = NOWORD -- reinitialize
for i=1,MAXGEN do
local list = statetab[prefix(w1, w2)]
-- choose a random item from list
local r = math.random(table.getn(list))
local nextword = list[r]
if nextword == NOWORD then return end
io.write(nextword, " ")
w1 = w2; w2 = nextword
end
  • 本文标题:《Lua编程》 第一篇:语言
  • 本文作者:形而上
  • 创建时间:2025-04-08 18:23:45
  • 本文链接:https://deepter.gitee.io/2025_04_08_lua/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!