Lua进程设计(第二版)
<h2 id="词法规范"><a href="https://lin-rudong.github.io/#%E8%AF%8D%E6%B3%95%E8%A7%84%E8%8C%83" class="headerlink" title="词法规范"></a>词法规范</h2><p>字母概念依赖于区域设置Locale。</p>
--
行注释,--[[
和]]
是块注释,当注释一段代码是,一个常见的技巧是将这些代码放入–[[和–]]中间,当重新启用这段代码时,只需在第一行行首添加-。
1 |
|
全局变量
全局变量不需要声明,只需将一个值赋予一个全局变量就可以创建来,访问一个为初始化的变量不会引发错误,访问结果是一个特殊的值nil。
如果一定要删除某个全局变量的话,只需将其赋值为nil。
解释器进程
用法是lua [选项参数] [脚本[参数]]。
选项参数e可以直接在命令行中输入代码,l用于加载库文档,i在运行完其他命令参数后进入交互模式。
解释器会用所有的参数创建一个名为arg的table,脚本名称位于索引0上。
类型与值
Lua是一种动态类型的语言,在语言中没有类型定义的语法,每个值都携带路它自身的类型信息。
有8种基础类型:nil、boolean、number、string、userdata(自定义类型)、function、thread和table。
变量没有预定义的类型,任何变量都可以包含任何类型的值。
在Lua种,函数是作为第一类值来看待的,可以像操作其他值一样来操作一个函数值。
nil
nil是一种类型,它只有一个值nil,它的主要功能是用于区别其他任何值,Lua将nil用于表示一种无效值的情况。
boolean
在Lua中任何值都可以表示一个条件,Lua将值false和nil视为假,除此之外的其他值视为真,包括数字零和空字符串。
number
number类型用于表示实数,Lua没有整数类型,因为没有必要,只要使用一个双精度来表示一个整数,就不会出现四舍五入的错误,因此,Lua中的数字可以表示任何32位整数,而不会产生四舍五入的错误,此外,大多数CPU的浮点数运算速度和整数运算一样快。
string
Lua的字符串是不可变的值。
Lua的字符串和其他对象一样,都是自动内存管理机制所管理的对象,无须担心字符串的分配和释放。
字面字符串用单引号或双引号来界定。
还可以用双方括号来界定一个字母字符串,这种形式可以延伸多行,Lua不会解释其中的转义序列。
如果字符串需要包含双方括号,可以在双方括号中加上任意数量的等号,这套机制同样适用于注释,可以用来注释已经包含了注释块的代码。
Lua提供来运行时的数字与字符串的自动转换,在一个字符串上应用算数操作时,Lua会尝试将这个字符串转换成一个数字,Lua不仅在算数操作中会强制转换,还会在其他任何需要数字的地方这么做,相反,在Lua期望一个字符串但却得到一个数字时,它也会将数字转换成字符串。
..
是字符串连接操作符,如果在数字后面输入它的时候,要用一个空格隔开,不然会将第一个点理解为小数点。
数字和字符串的显示转换可以通过函数tonumber和tostring来实现。
table
table类型实现来关联数组,关联数组是一种具有特殊索引方式的数组,不仅可以通过整数来索引它,还可以使用字符串或其他除了nil类型的值来索引,table没有固定的大小,table是Lua中仅有的数据结构机制,可以用来表示其他数组结构,Lua也是通过table来表示模块、包和对象的。
table不是值也不是变量,而是对象,table永远是匿名的,一个持有table的变量和table自身之间没有固定的关联性。
可以用任何数字作为数组索引的起始值,但就Lua的习惯而言,数组通常以1作为索引的起始值。
长度操作符#
用于返回一个数组或线性表的最后一个索引值,Lua将nil元素作为界定数组结尾的标志。
function
函数是作为第一类值来看待的,这表示函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值。
userdata(自定义类型)和thread
表达式
有别于传统的是,表达式中还可以包括函数定义和table构造式。
算术操作符
^
指数操作符。
关系操作符
~=
不等性测试,nil只与其自身相等,对于table、userdata和函数,Lua是作引用比较的,只有当它们引用同一个对象时,才认为它们相等。
数字和字符串之外的其他类型只能进行相等性或不等性比较。
逻辑操作符
逻辑操作符有and
、or
和not
,and和or都使用短路求值,对于and来说,如果它的第一个操作数为假,就返回第一个操作数,不然返回第二个操作数,对于or来说,如果它的第一个操作数为真,就返回第一个操作数,不然返回第二个操作数。
1 | 4 and 5 |
字符串连接
..
连接字符串。
优先级
操作符的优先级。
^ |
---|
not # - |
* / % |
+ - |
.. |
< > <= >= ~= == |
and |
or |
在二元操作符中,除了^和..是右结合的,所有其他操作符都是左结合的。
table构造式
最简单的构造式就是一个空构造式{}
,用于创建一个空table,构造式还可以用于初始化数组。
1 | days={'1','2','3','4','5','6','7'} |
记录风格的初始化和列表风格的初始化可以混合在一个构造式中使用,但是不能使用负数的索引,也不能用运算符作为记录的字段名,为了满足这些要求,Lua还提供来一种更通用的格式,这种格式允许在方括号之间,显示地用一个表达式来初始化索引值。
1 | opnames={ |
可以在最后一个元素后面写一个逗号,这个特性是可选的也是合法的,在一个构造式中还可以用分号代替逗号,通常会将分号用于分隔构造式中不同的成分,例如将列表部分与记录部分明显地区分开。
语句
Lua支持常规语句,包括赋值、控制和过程调用,另外Lua还支持一些不太常见的语句,例如多重赋值和局部变量声明。
赋值
多重赋值,将多个值赋予多个变量,x,y=y,x
交换x与y。
Lua总是会将等号右边值的个数调整到与左边变量的个数相一致,规则是,若值的个数少于变量的个数,那么多余的变量会被赋为nil,若值的个数更多的haul,那么多余的值会被丢弃。
局部变量与块
通过local
语句来创建局部变量,与全局变量不同的是,局部变量的作用于仅限于声明它们的那个快,一个块是一个控制结构的执行体、或者是一个函数的执行体再或者是一个进程块。
do-end
会创建一个进程块,尽可能地使用局部变量是一种良好的编程风格。
所声明的局部变量的作用于从声明语句开始,直至所在块的结尾。
控制结构
用于条件执行的if,用于迭代的while、repeat和for,所有控制结构都有一个显式的终止符,if、for和while以end作为结尾,repeat以until作为结尾。
if then else
1 | if a<0 then |
若要编写嵌套的if可以使用elseif
。
Lua不支持switch语句。
while
先测试while的条件,如果条件为假,那么循环结束,不然执行循环体。
1 | i=10 |
repeat
重复执行其循环体直到条件为真时结束,测试是在循环体之后做的,因此循环体至少会执行一次。
1 | i=10 |
与其他大多数语言不同的是,在Lua中,一个声明在循环体中的局部变量的作用于包括来条件测试。
数字型for
for语句有两种形式,数字型for和泛型for。
1 | --数字型for |
for的3个表达式是在循环开始前一次性求值的,控制变量会被自动地声明为for语句的局部变量,如果需要在循环结束后访问控制变量的值,必须将该值保存到另一个变量中。
不要在循环过程中修改控制变量的值,否则会导致不可预知的效果,break可以提前终止循环。
泛型for
通过一个迭代器函数来遍历所有值。
1 | for i,v in ipairs(a) do |
for循环的相同点:
- 循环变量是循环体的局部变量;
- 绝不应该对循环变量作任何赋值。
break与return
由于语法构造的原因,break或return只能是一个块的最后一条语句。
函数
一个函数若只有一个参数,并且此参数是一个字面字符串或table构造式,那么圆括号便是可有可无。
一个Lua进程既可以使用以Lua编写的函数,又可以调用以C语言编写的函数。
调用函数时提供的实参数量可以与形参数量不同,Lua会自动调整实参的数量,以匹配参数表的要求,这项调整与多重赋值很相似。
1 | function (n) |
该函数以1作为默认实参。
多重返回值
当一个函数调用作为另一个函数调用的最后一个实参时,第一个函数的所有返回值都将作为实参传入第二个函数,当函数出现在一个表达式中时,Lua会将其返回值数量调整为1。
table构造式可以完整地接收一个函数调用的所有结果。
也可以将一个函数调用放入一对圆括号中,从而迫使它只返回一个结果。
unpack函数,它接受一个数组作为参数,并从下标1开始返回该数组的所有元素。
变长参数
参数表中的3个点...
表示该函数可接受不同数量的实参,一个函数要访问它的变长参数时,仍需用到3个点作为表达式,{...}
表示一个由所有变长参数构成的数组。
具有变长参数的函数同样也可以拥有过任意数量的固定参数,但固定参数必须放在变长参数之前。
当变长参数包含nil时,可以用函数select来访问变长参数。
1 | select('#,...) --所有变长参数的总数,包括nil。 |
深入函数
函数是一种第一类值,它们具有特定的词法域。
第一类值的意思是表示在Lua中函数和其他传统类型的值具有相同的权利,函数可以存储到变量中或table中,可以作为实参传递给其他函数,还可以作为其他函数的返回值。
词法域的意思是指一个函数可以嵌套在另一个函数中,内部的函数可以访问外部函数中的变量,它允许在Lua中应用各种函数式语言中的强大编程技术。
函数定义是一种语法糖,本质是一条赋值语句,这条语句创建了一种类型为函数的值,并将这个值赋予一个变量。
1 | function () |
函数接受另一个函数作为实参的,称其是一个高阶函数。
closure(闭合函数)
外部函数的局部变量,在内部函数内既不是全局变量也不是局部变量,称为非局部的变量。
非局部变量在内部函数内可能会超出作用范围,Lua会以closure的概念来正确地处理这种情况,简单地讲,一个closure就是一个函数加上该函数所需访问的所有非局部的变量。
同一个函数可以创建多个不同的closure,它们各自拥有的非局部的变量都是独立的。
从技术上讲,Lua中只有closure,而不存在函数,因为函数本身就是一种特殊的closure。
非全局的函数
在定义递归的局部函数时,采用了基本函数定义语法的代码多数是错误的。
1 | local fact=function(n) |
当Lua编译到函数体中调用fact(n-1)的地方时,由于局部的fact尚未定义完毕,因此这句表达式其实是调用了一个全局的fact。
1 | local fact |
现在函数中的fact调用就表示了局部变量,即使在函数定义时,这个局部变量的值尚未完成定义,但之后在函数执行时,fact则肯定已经拥有了正确的值。
当Lua展开局部函数定义的语法糖时,并不是使用基本函数定义语法,因此,使用这种语法来定义递归函数不会产生错误。
1 | local function fact(n) |
这个技巧对于间接递归的函数而言是无效的,在间接递归的情况中,必须使用一个明确的前向声明。
1 | local f,g |
正确的尾调用
当一个函数调用是另一个函数的最后一个动作时,该调用是一条尾调用,进程不需要返回那个尾调用所在的函数,所以在尾调用之后,进程也不需要保存任何关于该函数的栈信息,返回时,执行控制权可以直接返回到上一级,使得在进行尾调用时不耗费任何栈空间,这种实现称为支持尾调用消除。
由于尾调用不会耗费栈空间,所以一个进程可以拥有无数嵌套的尾调用,在调用以下函数时,传入任何数字作为参数都不会造成栈溢出。
1 | function foo(n) |
function f(x) g(x) end
不是一条尾调用,当调用完g后,f并不能立即返回,它还需要丢弃g返回的临时结果。
1 | return g(x)+1 --必须做一次假发 |
在Lua中,return <func>(<args>)
这样的调用形式才算是一条尾调用,Lua会在调用前对<func>及其参数求值,所以它们可以是任意复杂的表达式。
迭代器与泛型for
迭代器与closure
在Lua中,通常将迭代器表示为函数,每调用一次函数,即返回集合中下一个元素。
每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何步进到下一个位置。为了创建一个新的closure,还必须创建它的非局部的变量,因此一个closure结构通常涉及到两个函数,closure本身和一个用于创建该closure的工厂函数。
泛型for每次新迭代时调用迭代器,并在迭代器返回nil时结束循环。
泛型for的语义
泛型for在循环过程内部保存了迭代器函数,实际上它保存着3个值