Chunks

在Lua中,chunk是lua一系列语句的集合。Lua的语句结尾加分号分割,虽然语言上没有强制要求,但是每一个语句后面都加上分号会让程序的逻辑更加清晰。Lua的命令行终端为我们提供了组合运行多个chunk的方式。

  1. lua -la -lb 这个命令会搜索a文件和b文件,并且分别运行这两个文件中的chunk
  2. 交互模式下调用dofile函数,可以把一个lua脚本文件中包含的chunk加载进来,然后无缝的调用。

全局变量

Lua中全局变量的行为和python中的普通变量是一样的,我们可以在任何需要某个全局变量的地方直接使用它。如果该全局变量之前没有被初始化,那么这个变量的值就是nil,证明这个变量是不存在的。要删除这个变量的时候也是直接给这个变量赋值为nil即可。只要我们给某个全局变量赋值了,那么就算是创建了这个全局变量。在Lua中,创建全局变量是不要事先声明的,直接赋值即可。

命令行参数

Lua脚本在解析命令行参数的时候,以脚本名这个参数为起点,下标为0,右侧以此递增,左侧以此递减。

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

构造一个命令行参数列表的时候,argv[1]为a,argv[-1]为sin=math.sin。其余的以此类推。

数据类型

Lua的数据类型和Python一样,都是动态的,不需要事先定义好某一个变量的数据类型,这点和go是不同的。Lua中也有一种预定义的数据类型叫function,那也就是说,在lua中也可以像在go中那样,把一个函数赋值给某一个变量,之后就可以像使用这个函数一样使用这个变量。

Nil

一个没有经过赋值的全局变量,他的值就是nil,类型为Nil。如果想回收或者删除某个全局变量,那么直接将这个全局变量设置为nil即可。

Booleans

Lua中的布尔类型和golang中的含义有所不同。golang中的布尔类型只有true和false两种,其余的整数或者是非零值等都不会自动被转换成相对应的布尔值,但是Lua中是这样的。Lua中除了false和nil为假之外,其他值都为真,这其中包括0或者空字符串。

Strings

Lua的字符串和golang中的一样,都是一经定义不允许修改。并且在Lua中,字符串可以使用单双引号来分割,特殊情况下可以使用[[…]]来表示字符串,它类似python中的三引号,支持多行扩展,且不会翻译字符串中的转移序列,如果第一个字符是换行符还会被忽略掉。

Lua中当一个字符串参与算数操作的时候,会将其转换为数字,执行相应的计算。如果字符串不能成功转换成数字,那么会报错。Lua中的字符串连接符不是+,而是*..*两个点号。所以,如果你想通过链接的方式将一个数字和字符串链接成一个字符串的时候,就可以使用它。虽然Lua中字符串可以在进行算数操作的时候转换为数字,但是将一个字符串和数字进行比较是错误的,应该使用tonumber函数将一个字符串转换为数字。如果转换失败,tonumber会返回一个nil值。

表达式

关系运算

lua中的==和~=分别用于比较两个操作数相等还是不相等。==在比较同类型数据的时候会比较他们的值,比较不同类型的数据的时候会直接返回false。Lua在比较引用类型对象的时候会检查他们所引用的对象是否是同一个。nil值只会和自己相等。

逻辑运算

Lua中的逻辑运算和其他一些语言中的逻辑运算的结果是不一样的,逻辑运算符认为 false 和 nil 是假(false),其他为真,0 也是 true。python中的and和or运算符最后计算的结果都是True或者False。但是在Lua中有一套特殊的规则:

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

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

1
x = x or v

但是在Lua中,not 的结果只返回 false 或者 true

表的构造

Lua中有一个数据类型是table。最简单的构造一个table的形式就是{}。Lua中的表更像是一个大集合,它里面可以存储一切Lua支持的数据类型的数据。表中的成员可以按照key-value的形式存储(record-style),也可以使用索引(list-style)的方式。在读取表中数据的时候,即可以按照key来获取对应的元素,也可以使用索引的方式去获取之前存储在table中的list内的元素。recore_style和list-style是可以混用的。

语句

赋值语句

Lua的赋值语句有几处比较特殊的地方:

  1. Lua的赋值语句支持多个值一起赋值,赋值语句会先将等号右边的所有的值计算出来,然后一次性赋值给左边的变量。
  2. 等号左边的变量数量和等号右边值的数量可以不用一一对应。如果待赋值变量数量大于值的数量,那么没有对应的变量的值是nil。如果值的数量大于等号左边变量的数量,那么多出来的值将会被直接忽略。
  3. 与golang不同,当我们想给多个变量赋值同一个值的时候,golang中: a,b,c=10,lua中: a,b,c=10, 10, 10

局部变量

在Lua中,默认直接赋值创建的变量都是全局变量。全局变量在整个代码文件的作用域中都是有效的。局部变量的定义方式和全局变量不同,变量名前要加local关键字,并且一个局部变量可能仅仅只是在某个代码块中是有效的。

控制结构

lua中常见的控制结构有几种:if-else, while, for, repeat。这几种控制结构在开头和结尾的地方都有明显的标识,起点是do,终点是end。其中repeat比较特殊,像c语言中的do/while结构是执行body一次,如果condition为true则继续执行,而Lua中的repeat是只要condition为false就继续执行body,直到condition为true。

循环语句

Lua中提供了四种形式的循环语句

  1. while
  2. repeat
  3. numeric for
  4. generic for

其中while和其他语言中的语义是相同的,repeat在上面也介绍过了。下面主要来讲一下两种形式的for语句的使用方法和区别。

numeric for

numeric for循环是符合一般编程语言中的for循环形式的,它也是有三个表达式,第一个是初始化,第二个是判定条件,第三个是迭代的步长。第三项是可以忽略的,默认迭代的步长是1。与一般语言中的for循环不同之处是,判定条件的表达式的结果必须是number类型,而不能是布尔类型。如果想要用numeric for来实现一个死循环的话,可以使用math.huge作为循环的判定条件。

使用的numeric for循环语句的注意事项有一下三点:

  1. for循环中的三个表达式,都是在循环开始之前仅计算一次
  2. for循环中的变量是局部变量,进入循环之后自动创建的,其作用于只限于这个循环,如果在后面的逻辑中想使用这个变量的值需要在循环退出之前将它保存起来。
  3. 退出循环的时候,不要通过改变循环变量的方式,应该使用break

generic for

generic for循环类似于python中的in-range形式,golang中的value-range形式,总的来说就是迭代某一个集合中的所有元素。这种类型的循环依赖于一个迭代器的实现,这个迭代器在lua的标准库中有一些已有的实现,如按行读取一个文件,按记录读取一个table,按单词读取一个字符串等。

generic for循环和numeric for循环有两处在使用上都需要注意的地方

  1. 循环当中使用的迭代变量是局部的,作用于仅限于这个循环
  2. 不要在循环中改动迭代变量

break, return and goto

break语句用于在循环中中断循环的执行,不能够在循环外使用。Lua中的函数在函数尾部默认会有一个隐式的return语句,我们不需要特意的写一个。

Lua中的return语句和其他语言中的有所不同,比如在golang中或者python中,return语句是可以出现在任何地方的,但是在Lua中return语句只能出现在某一段代码块的末尾或者在end,else,util之前。所以,在debug过程中如果想测试一个函数是否被调用,但是不想测试它的内部逻辑,可以在函数体的开头处加上。即想在任意一个位置执行return的时候,都需要把它包含在do..end里面。

1
2
3
do
	return
end

函数

函数在接受参数的时候,按正常来讲,传递进来的参数和函数想要接收的参数数量应该是相同的。但是在Lua中,允许二者的数量是不同的。如果传递进来的参数多余函数的形参列表,那么从左到右按照顺序匹配,多余的自动丢弃。如果传递进来的参数小于函数接收的参数数量,从左到右进行匹配,未经赋值的参数的值为nil。虽然我们应该尽量避免实际参数和形式参数数量不同的问题,但是正是由于lua这种机制,以及之前提到的or逻辑运算,可以实现默认参数的功能。

多重返回值

Lua中的函数可以一次性返回多个返回值。但是调用lua中的函数在不同的地方,返回值的数量也是不一样的。

  1. 在多重赋值的过程当中,如果函数的调用是等号右边唯一的一个表达式,那么此时,函数将返回它所有数量的返回值,对应等号左边的变量会接受赋值,多余的丢弃。
  2. 如果函数的调用不是等号右边的唯一一个表达式的话,仅会返回多个返回值中的第一个
  3. 当返回多个返回值的函数在某个函数调用传递参数的时候被调用,如果它是唯一一个函数参数,那么其所有的返回值都会被当做是函数的参数。但是,如果函数的调用处于一个表达式中的话,那么还是只会用该函数返回值的第一个。

Lua中返回值还有一个比较特殊的地方,就是当我们对一个返回多值的函数调用外面再加上一个括号的时候,它就只返回多个返回值中的第一个。

Lua中的函数在接收可变参数列表之后,如果需要迭代这个列表的话,可以把这个列表里的参数放在一个table当中。如果直接使用{…}的形式来构造一个table的话,那么如果可变参数列表中有nil值的出现,这个nil值就会被自动的过滤掉,最后的table当中是没有这个nil值的。但是使用table.pack函数的话,就可以无损的把可变参数列表转换到table当中。

命名参数

给调用的函数传递参数的过程当中是按照位置来匹配的,如果被调用函数参数过多的话,我们又没办法记住他们的先后顺序,python当中就会提供一种可指定函数参数名赋值的机制。但是在Lua中并没有明确提供这种机制,但是使用table我们可以实现差不多的效果。将我们要传递的参数按照key-value的形式存放在一个table当中,然后我们自己对一些标准库需要多个参数的函数进行封装,封装的函数只接受一个参数,就是我们要传递进来的table,这样一来,我们只需要明确的指定一次参数的传递顺序,以后再使用某个标准库函数的时候,就可以直接调用我们封装好的,而不必在意参数的传递顺序。

函数值

lua中的函数是具有一定作用域的一类值。它和其他类型的值一样,可以赋值给变量,并且函数值本身是匿名的,我们之前所看到的function关键字后面所谓的函数名实际上是一个变量,它指向的内存位置中存储了一个函数值。由此可见,在代码中我们可以给同一个”函数变量”赋值多个“函数值”。如可以给print赋值一个新的函数值。

实际上lua中的定义一个函数的时候,是创建了一个function类型的数据并把它赋值给了一个变量。

非全局函数

非全局函数即为局部函数,局部函数的定义和全局函数的定义无太大差别,在function前面加上local关键字即可。还可以先定义一个local变脸,然后用匿名的函数去初始化它。

先定义一个local变量然后用一个匿名函数初始化,这样虽然可以定义一个局部函数,但是如果局部函数内涉及到对自己的地柜调用,此时编译时不通过的。因为在定义的过程当中,函数逻辑中对本身的调用失去找的同名的全局变量。改进这种问题的办法也很简单,可以先定义一个local变量,然后把匿名函数赋值给他,而不是初始化。另外一种办法是按照上面说的第一种形式来定义一个局部函数。

尾递归

Lua中对尾递归的支持很好,它在执行函数内尾递归调用的时候,会自动清除调用者在栈中的所有信息。当被调用者计算完毕的时候,它的结果会直接被返回到当时它的调用者被调用的位置。因为lua语言实现尾递归的时候,是不需要额外的栈空间的,所以lua语言中的尾递归调用理论上是可以无线递归下去的。

除了了解尾递归在lua语言中的行为,还有一个值得我们注意的就是到底什么样的递归调用才算做是尾递归。我觉得总结起来就是一句话:return语句后面只能接收一个单独的函数调用,且对这个函数调用的结果不能进行任何形式的计算,即使在函数调用的外部加一层括号也是不行的。