Lua语言入门

遵照惯例,我们的第一个Lua程序时通过标准输出打印字符串”Hello World”:
print("Hello World")

如果读者使用的是Lua语言独立解释器(stand-alone interpreter),要运行这第一个程序的话,直接调用解释器运行包含程序代码的文本文件就可以了。例如,如果把上述代码保存为名hello.lua的文件,那么可以通过以下命令运行:
% lua hello.lua

再来看一个稍微复杂点的例子,以下代码定义了一个计算阶乘的函数,该函数先让用户输入一个数,然后打印出这个数的阶乘结果:

1
2
3
4
5
6
7
8
9
10
11
12
--定义一个计算阶乘的函数
function fact(n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end
print("enter a number:")

a = io.read("*n") --读取一个数字
print(fact(a))

1.1 程序段

我们将Lua语言执行的每一段代码称为一个程序段(Chunk),即一组命令或表达式组成的序列。
程序段既可以简单到只由一句表达式构成,也可以由多句表达式和函数定义(实际是复制表达式,后面会详细介绍)组成(例如计算阶乘的示例)。程序段在大小上并没有限制,事实上,由于Lua语言也可以被用作数据定义语言,所以几MB的程序段也很常见。Lua语言的解释器可以支持非常大的程序段。
除了将源码保存成文件外,我们有也可以直接在交互式模式(interactive mode)下运行独立解释器。当不带参数第调用lua时,可以看到如下的输出:

1
2
3
% lua
Lua 5.3 Copyright(C)1994-2016 Lua.org, PUC-Rio
>

此后,输入的每一条命令都会在按下回车键后立即执行。我们可以通过输入EPF控制字符,或调用操作系统库的exit函数(执行os.exit())退出交互模式。
从Lua5.3版本开始,可以直接在交互模式下输入表达式,Lua语言会输出表达式的值,例如:
1
2
3
4
5
6
% lua
Lua 5.3 Copyright(C)1994-2016 Lua.org, PUC-Rio
> math.pi / 4 --0.78539816339745
> a = 15
> a^2 --225
> a + 2 --7

要以代码段的方式运行代码(不在交互模式下),那么必须把表达式包在函数print的调用中:
1
2
3
4
print(math.pi/4)
a = 15
print(a^2)
print(a + 2)

在交互模式下,Lua语言解释器一般会把我们输入的每一行当做完整的程序块或表达式解释执行。但是,如果Lua语言解释器发现我们输入的某一行不完成,那么它会等待直到程序块或表达式被输入完整后再进行解释执行。这样,我们也可以直接在交互模式下输入一个像阶乘函数示例那样的由多行组成的多行定义。不过,对于这种较长的函数定义而言,将其保存文件然后再调用独立解释器来执行通常更方便。
我们可以使用-i参数让Lua语言解释器在执行完制定的程序段后进入交互模式:
% lua -i prog
上述命令会在执行完文件prog中的程序后进入交互模式,这对于调试和手工测试很有用。在本章的最后,我们会学习有关独立解释器的更多参数。
另一种运行程序段的方式调用函数dofile,该函数会立即执行一个文件。例如,假设我们有一个如下所示的文件lib1.lua:
1
2
3
4
5
6
7
function norm( x,y )
return math.sqrt(x^2 + y^2)
end

function twice (x)
return 2.0 * x
end

然后,在交互模式下运行:
1
2
3
>dofile('lib1.lua')   --加载文件
>n = norm(3.4,1.0)
>twice(n) --7.088018058677

函数dofile在开发阶段也非常有用。我们可以同时打开两个窗口,一个窗口中使用文件编辑器编辑代码,另一个窗口中使用交互模式运行Lua语言解释器。当修改完代码并保存后,只要在Lua语言交互模式的提示符下执行dofile(“prog.lua”)就可以加载新代码,然后就可以观察新代码的函数调用和执行结果了。

1.2 一些语法规范

Lua语言中的标识符是由任意字母、数字和下划线组成的字符串(注意不能用数字开头),例如:
i,j,i1,j1,_i,abc
“下划线 + 大写字母”组成的标识符通过被Lua语言用作特殊用途,应避免将其用作其他用途。我们通常将“下划线 + 小写字母”用作哑变量。
以下是Lua语言的保留字,它们不能被用作标识符:
and,brask,do,else,elseif,end,false,goto,for,function,if,in,local,nil,not,or,repeat,return,then,true,until,while
Lua语言是对大小写敏感的,因而and是保留字,但是AndAND就是两个不同的标识符。
Lua语言中使用两个连续的连字符(—)表示单行注释,使用两个连续的连字符加两对左方括号表示长注释或多行注释的开始,指导两个连续的右括号为止,中间都是注释,例如:

1
2
3
--[[多行
长注释
]]

在注释一段代码时,一个常见的技巧是将这些代码放入—[[和—]]之间,例如:
1
2
3
--[[
print(10) --无动作,被注释掉了
--]]

当我们需要重新启用这段代码时,只需要在第一行行首添加一个连字符即可:
1
2
3
---[[
print(10) --10
--]]

在第一个示例中,第一行的—[[表示一段多行注释的开始,直到两个连续的右括号这段多行注释才会结束,因而尽管最后一行有两个连续的连字符,但由于这两个连字符在最后两个右括号之前,所以仍然被注释掉了。在第二个示例中,由于第一行的—[[实际是单行注释,因此最后一行实际上也是一条独立的单行注释(最后的两个连续右方括号没有与之匹配的—[[),print并没有被注释掉。
在Lua语言中,连续语句之间的分隔符并不是必需的,如果有需要的话可以使用分号来进行分割。在Lua语言中,表达式之间的换行也不起任何作用。例如,以下4个程序段都是合法且等价的:
1
2
3
4
5
6
7
8
9
a = 1 
b = a * 2

a = 1;
b = a * 2;

a = 1; b = a * 2

a = 1 b = a * 2

这个根据个人习惯来。

1.3 全局变量

在Lua语言中,全局变量无须声明即可使用,使用未经初始化的全局变量也不会导致错误。当使用未经初始化的全局变量时,得到的结果是nil

1
2
3
> b      -- nil
> b = 10
> b --10

当把nil赋值给全局变量时,Lua会回收改全局变量(就像该全局变量从来没有出现过一样),例如:
1
2
> b = nil
> b --nil

Lua语言不区分为初始化变量和被赋值为nil的变量。在上述赋值语句执行后,Lua语言会最终回收该变量占用的内存。

1.4类型和值

Lua语言是一种动态类型的语言,在这种语言中没有类型定义,每个值都带有其自身的类型信息。
Lua语言中有8个基本类型:nil(空)、bloolean(布尔)、number(数值)、string(字符串)、userdata(用户数据)、function(函数)、thread(线程)和table(表)。使用函数type可获取一个值对应的类型名称:

1
2
3
4
5
6
7
8
9
>type(nil)			-- nil
>type(true) -- boolean
>type(10.4*3) --number
>type("Hello World")--string
>type(io.stdin) --userdata
>type(print) --function
>type(type) --thread
>type({}) --table
>type(type(X)) --string

不管X是什么,最后一行返回的永远是“string”。这是因为函数type的返回值永远是一个字符串。
userdata类型允许把任意的C语言数据保存在Lua语言变量中。在Lua语言中,用户数据类型除了赋值和相等性测试外,没有其他预定义的操作。用户数据被用来表示由应用或C语言编写的库所创建的新类型。例如,标准I/O库使用用户数据来表示打开的文件。我们会在后面设计C API时再讨论更多的相关内容。
变量没有预定义的类型,任何变量都可以包含任何类型的值:
1
2
3
4
5
6
7
>type(a)		-- nil(a没有初始化)
>a = 10
>type(a) --number
>a = "a string!"
>type(a) --string
>a = nil
>type(a) --nil

一般情况下,将一个变量用作不同类型时会导致代码的可读性不佳;但是,在某些情况下谨慎地使用这个特行可能会带来一定程度的便利。例如,当代码发生异常时可以返回一个nil以区别于其他正常情况下的返回值。

1.4.1 nil

nil是一种只有一个nil值的类型,它的主要作用就是与其他所有值进行区分。Lua语言使用nil来表示无效值的情况。像我们所学习的其他语言,一个全局变量在第一次被赋值前的默认值就是nil,而将nil赋值给全局变量则相当于将其删除。

1.4.2 boolean

boolean类型具有两个值,truefalse,它们分别代表了传统布尔值。不过,在Lua语言中,Boolean值并非是用于条件测试的唯一方式,任何值都可以表示条件。在Lua语言中,条件测试将除Boolean值false和nil外的所有其他值视为真。特别的是,在条件检测中Lua语言把零和空字符串也都视为真。
Lua语言支持常见的逻辑运算符:and,ornot。和条检测试一样,所有的逻辑运算将Boolean类型的false和nil当做假,而把其他值当作真。

1.5 独立解释器

独立解释器是一个可以直接使用Lua语言的小程序。
如果源代码文件第一行以井号(#)开头,那么解释器在加载该文件时会忽略这一行。这个特征主要是为了方便在POSIX系统中将Lua作为一种脚本解释器来使用。假设独立解释器位于/usr/local/bin下,当使用下列脚本:
#!/usr/local/bin/lua

#!/usr/bin/env lua
时,不需要显式地调用Lua语言解释器也可以直接运行Lua脚本。
lua命令的完整参数如:
lua [options] [script [args]]
其中,所有的参数都是可选的。如前所述,当不使用任何参数调用lua时,就会直接进入交互模式。
-e参数允许我们直接在命令行中输入代码,例如

1
% lua -e "print(math.sin(12))" 

请注意,在POSIX系统下需要使用双引号,以防止Shell错误第解析括号。
-l参数用于加载库。正如之前提到的那样,-i参数用于在运行完其他命令行参数后进入交互模式。因此,下面的命令会首先加载lib库,然后执行x=10的赋值语句,并最终进入交互模式:
1
% lua -i -llib -e "x = 10"

如果在交互模式下输入表达式,那么解释器会输出表达式求值后的结果:
1
2
3
> math.sin(3)
> a = 30
> a --30

请记住,这个特征只在Lua5.3及之后的版本中才有效。在之前的版本中,必须在表达式前加上一个等号。如果不想输出结果,那么可以在行末加上一个分号:
1
2
> io.flush()	--true
> io.flush();

分号使得最后一行在语法上变成了无效的表达式,但可以被当作有效命令执行。
解释器在处理参数前,会查找名为LUA_INIT_5_3的环境变量,如果找不到,就会再查找名为LUA_INIT的环境变量。如果这两个环境变量中的任意一个存在,并且其内容为@filename,那么解释器就会运行相应的文件;如果这两个环境变量存在,但是不以@开头,那么解释器就会认为其包含Lua代码,并会对其进行解释执行。由于可以通过上面的方法完整地配置Lua,因而LUA_INIT使得我们可以灵活地配置独立解释器。例如,我们可以预先加载程序包、修改路径、定义自定义函数、对函数进行重命名或删除函数,等等。
我们可以通过预先定义的全局变量arg来获取解释器传入的参数。例如,当执行如下命令时:
1
% lua script a b c

编辑器在运行代码前或创建一个名为arg的表,其中存储了所有的命令行参数。索引0中保存的内容为脚本名,索引1中保存的内容为第一个参数,以此类推;而在脚本之前的所有选项则位于负数索引上,例如:
1
% lua -e "sin = math.sin" script a b

解释器按照如下的方式获取参数:
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"

一般情况下,脚本只会用到索引为证书的参数。
Lua语言也支持可变长参数,可以通过可变长参数表达式来获取。在脚本文件中,表达式…(3个点)表示传递给脚本的所有参数。