第2章 探讨Lua基础知识
Lua语言与您所了解的其他编程语言比较接近,它相对较小。在Lua的设计与发展过程中,Lua从许多不同的语言中得到了灵感,包括Lisp、Scheme、Pascal和C等。
本章内容是对Lua编程语言的一个概述,如果您在这之前已经有Lua或者其他编程语言的使用经验,您可能会希望浏览本章然后选做一些相关的练习。
您可以从www.lua.org中阅读到更多Lua编程语言的相关资料。这个网站有很多参考资料,包括Programing in Lua一书的在线版,这是一部完全关于Lua编程语言的书。
2.1 使用Lua解释器
您可以使用第1章已安装的Lua解释器来运行本章的所有例子和习题。
2.1.1 运行命令
Lua解释器是交互的,您可以输入命令,并得到回应,就像两个朋友在交谈一样。(默然说话:这让我想起了上初中时使用过的Basic解释器。。。。已逝的年华,不再复还的时光。。。。)对于代码中的错误,您会接收到一个即时的反馈,使您可以通过修正该语言来了解它是如何动作的。
在提示符下输入下面的命令(您只需要输入“>”后面的部分):
>print(“Hello 艾泽拉斯!”)
您会看见下面的输出:
Hello 艾泽拉斯
>
2.1.2 错误信息的理解
在运行命令的时候,偶尔会出现输入错误并从Lua中获得一个错误反馈。错误信息通常是通俗易懂(默然说话:此话针对美国人,对于我们中国人。。。。。。唉,让人爱不起来的英文字母呀。。。。如果你的英语不太好,其实关系并不大,你完全可以通过学习计算机把英语学好——我就是活生生的例子。不要放弃这个一举两得的机会哦!),并且能指出哪里出了问题。在提示符中输入如下命令(注意要故意拼错print这个单词):
> prnit("Hello 艾泽拉斯")
得到的响应是一个跟下面差不多的典型的Lua错误信息:
stdin:1: attempt to call global 'prnit' (a nil value)
stack traceback:
stdin:1: in main chunk
[C]: ?
>
第一行指出错误信息和错误发生的行号(默然说话:就是“stdin:1”,stdin表示你的输入,1就是指第一行——我们也只输入了一行,不是么?)。后面解释了具体的错误原因:你调用了一个并不存在的函数(默然说话:你把函数的名字打错了,计算机当然会认为它不存在)。
剩余的错误信息叫栈跟踪(traceback),如果你用过Java等高级语言,你就知道在一个复杂的程序里面,栈跟踪对于迅速找到错误原因的重要性了。它能指出错误出现在哪里。
2.1.3 使用历史信息来做改变
如果你是通过上面的链接获得的Lua解释器WowLua,那么你可以通过上下箭头键来查看最近的命令行历史,也就是你最近在翻译器中打过的命令。
2.1.4 退出解释器
在Windows系统中,如果您已经完成解释器里面运行的代码,您可以简单地关闭窗口。当然,你也可以使用Ctrl+Z来退出解释器,只是在按完Ctrl+Z之后,你需要按一次回车,如果你不想按回车,使用Ctrl+C就可以直接退出。
2.2 处理数字
把下面的命令输入解释器:
>print(2+2)
您自然会看到系统响应一个4。
2.2.1 基本算术运算
表2-1 有效的算术运算符
运算 |
在Lua中的表示方法 |
举例 |
加 |
+ |
>print(4+4) 8 |
减 |
- |
>print(6-10) -8 |
乘 |
* |
>print(13*13) 169 |
除 |
/ |
>print(10/2) 5 |
幂 |
^ |
>print(13^2) 169 |
取模 |
% |
>print(8%3) 1 |
取负 |
- |
>print(-(4+4)) -8 |
2.2.2 科学计数法
当不方便显示一个很大的数时,Lua可以自动地用科学计数法来显示它。运行下面的命令:
> print(10^15)
1e+015
Lua为了将它们显示出来,先将它们转换成科学计数法。您也可以用这种方式来写一个数字,表示一个数的10的几次幂,幂指数为第二个数字(中间的e可以大写也可以小写)。例如:
> print(10^15)
1e+015
> print(1.23456e5)
123456
> print(1.23456*(10^5))
123456
> print(1234e-4)
0.1234
> print(1234*(10^-4))
0.1234
2.2.3 十六进制表示法
Lua可以自动地将十六进制表示的数字转换成十进制。十六进制表示以0x开头,后面接着一串有效的十六进制数字(0~F)。
> print(0x1)
1
> print(0xF)
15
> print(0x10)
16
> print(0x10a4)
4260
当编写代码的时候,您可以用这种格式来表示数字,Lua会正确地对它们进行转换。然而,Lua仅以十进制或科学计数法的形式来返回结果,而不管这些数是怎么输入的。
2.2.4 理解浮点
Lua系统中的数在内部都是用浮点表示的。一般情况下,这不会有什么问题,但有时却会有一些比较奇特的现象。比如下面这个例子。
> pointTwo=1.2-1.0
> print(pointTwo<0.2)
true
> print(pointTwo)
0.2
在这里,pointTwo是我们声明的一个变量,它存储了1.2-1.0的结果0.2,但由于这个0.2是被计算出来的,所以它会产生误差,而Lua在显示时又会纠正这个误差,所以我们看到了0.2<0.2的结果为true的奇怪现象。
(默然说话:其实这是一个非常普通存在于各种语言中的问题,问题的原因不在于语言本身,而在于计算机对于浮点数运算的方式,这是一个硬件的问题。问题改变了——对,是改变了,而非颠覆——我们以前认为“小数更精确”的看法。在计算机世界中,关于精确的描述,应该是这样:小数更精确,但进行两数的比较时,整数比较的结果会更精确,而非小数。所以我们应该尽力避开比较两个小数的大小,如果要进行小数的比较,必须设置有效数字进行比较,也就是设置一个误差范围,在误差范围内进行比较。)
关于浮点数问题的讨论,下面给出几个中文链接:
http://zh.wikipedia.org/wiki/IEEE_754
http://hi.baidu.com/abcserver/blog/item/e7ff32387a8e2c2cb9998fdb.html
http://blog.csdn.net/ttlyfast/archive/2007/09/12/1782524.aspx
2.3 理解值和变量
像很多其他的语言一样,Lua在值和变量之间进行了区分。了解值的种类,以及值和变量之间的内在区别对编程很有帮助。
2.3.1 探讨值和它们的类型
在Lua编程语言中有8种基本类型,您见过的所有值都应该属于它们中的一个。您已经看见了两种不同的类型,数和字符串。
1. 基本类型
表2-2 Lua的基本类型
类型 |
描述 |
数字(number) |
所有的数字(包括十六进制数和那些使用科学计数法的)都属于这一类。 |
字符串(string) |
一个字符序列 |
布尔值(boolean) |
true和false |
函数(function) |
函数是一个可以调用的语句集合,第3章会讲到。 |
表(table) |
表是传统的哈希表和数组的混合类型 |
线程(thread) |
线程类型的值是一个可用于异步计算的协同程序(轻量有限线程) |
用户数据(userdata) |
用户数据就是类似C语言中的结构体类型 |
2. 使用type()函数
在Lua中您可以使用type()函数来确定一个给定值的类型,它会给您在编程时进行确认和验证带来更大的灵活性。
> print(type(5))
number
> print(type("Hello 艾泽拉斯"))
string
> print(type(print))
function
> print(type(2*(1+2+3)^3))
number
> print(prnit)
nil
注意最后一个例子是拼错的print,这是一个nil值,所以type函数返回了一个nil。您总可以使用type()函数来找出某个值究竟属于什么类型。
2.3.2 使用变量
变量可以看作是一个Lua值的临时名称,而同一个值可以有多个名称。
1. 有效的变量名
变量名必须以下划线或者字母开头。名称的本身不能包含除字母、数字和下划线之外的其他字符。另外,它还不能是Lua中的关键字,即and、break、do、else、elseif、for、function、if、in、local、nil、not、or、repeat、return、then、true、until和while。变量名区分大小写。
2. 变量的赋值
可以使用赋值运算符“=”来将一个值绑定到一个变量名,变量名应该放在左边而它的值应该放在右边。
> foo=14
> print(foo)
14
> foo="Hello"
> print(foo)
Hello
3. 对多个变量赋值
在有些场合,您需要同时对多个变量进行赋值,下面这种便捷的形式会令赋值变得更加简单。运行下面的例子:
> x,y=3,5
> print(x*y)
15
赋值运算符允许一个变量列表出现在左边,而一个值的列表出现在右边。当要在函数中传入和返回值的时候,这条规则会比较有用。如果运算符左边的变量个数比右边的值个数要多,那么剩下的变量将会被赋予nil值。
4. 值的比较
表2-3 比较运算符
比较运算符 |
相应的Lua运算符 |
相等 |
== |
小于 |
< |
大于 |
> |
小于等于 |
<= |
大于等于 |
>= |
不等于 |
~= |
等价运算符(==和~=)可以用来比较任意类型的两个值,但其余运算符只能进行同类型的比较。否则你会得到如下的错误信息:
> print(1<"Hello")
stdin:1: attempt to compare number with string
stack traceback:
stdin:1: in main chunk
[C]: ?
2.4 使用字符串
1.4.1 比较字符串
> print("a"<"b")
true
> print("d"<"c")
false
> print("abcd"<"abce")
true
> print("a"<"A")
false
> print("abcd"<"abcde")
true
> print("rests"<"test")
true
大于和小于运算符可以在字符串上使用,但是结果将取决于系统内部对不同字符的排列次序。对于单个字符的比较,运算符比较两个字符在字符集中的次序;对于多个字符的比较,它比较两个字符串序列中第一个出现的不同字符。
2.4.2 多个字符串的连接
Lua的字符串是不可修改的,也就是说除非完全新建一个字符串,否则它是不可改变的。要连接两个字符串,您要使用一个特殊的连接运算符(..),它可以将两个字符串连接在一起产生一个新的、更长的字符串。下面是几个例子:
> x="Hello"
> y="艾泽拉斯"
> print(x..y)
Hello艾泽拉斯
> foo = "a".."b".."c".."d".."e"
> print(foo)
abcde
2.4.3 将数字转换成字符串
在许多情况下,Lua可以帮你处理这个问题。尝试下面的命令:
> print("Number "..4)
Number 4
Lua自动将4转换成了字符串,并将它加到字符串“Number”后面。如果您需要显式地将一个数字转换成字符串,可以使用tostring()函数,正如下面的例子所示:
> x=17
> foo=tostring(x)
> print(type(foo))
string
tostring()函数将任意的输入参数(在这个例子中是一个数字)转换成字符串值。
2.4.4 将字符串转换成数字
tonumber()函数可以满足您的这项要求,它可以将一个数字字符串转换为一个数。如果不能够转换这个字符串(例如字符串不包含数字),这个函数就返回nil。下面是一些例子:
--普通数字字符串转换为数字
> x=tonumber("1234")
> print(type(x))
number
> print(x)
1234
--科学计数法形式的数字字符串转换为数字
> x=tonumber("1e3")
> print(type(x))
number
> print(x)
1000
--数字与字符混合的字符串转换为nil
> x=tonumber("123number")
> print(type(x))
nil
--字符开头与数字混合的字符串(中间有空格)转换为nil
> x=tonumber("number 123")
> print(type(x))
nil
--数字加空格的字符串转换为数字
> x=tonumber("123 ")
> print(type(x))
number
> print(x)
123
--数字加字符串(中间有空格)转换为nil
> x=tonumber("123 number")
> print(type(x))
nil
(默然说话:以上是我自己所做的实验,发现tonumber函数只要在字符串中含有字符,就不能正常转换出数字,而是返回nil。这点与JavaScript不太相同,更多内容在第7章我们还会介绍)
2.4.5 引用字符串
使用双引号创建字符串是几乎所有编程语言的通行方式,但在Lua中还有其他几种方法可以构造字符串。除非有特殊的理由,否则我还是强烈建议你只使用双引号构建字符串(默然说话:不过我个人在脚本语言中比较倾向于使用单引号)。
1. 单引号
这是一个标准的习惯。看下面的例子:
> x='Hello'
> print(x)
Hello
> x='Isn/'t it nice?'
> print(x)
Isn't it nice?
如果你的字符串中本身就含有单引号,你可以通过在单引号前面加上反斜杠的方式告诉Lua,这个单引号是字符串的一部分,这样,Lua就能正确进行处理,而不是报出一个错误消息。
2. 双引号
双引号的用法单引号一样,同样的,如果你的字符串本身就含有双引号,你也要在双引号前面加上反斜杠。
3. 方括号
这是一种很特殊的用法,它允许您随意裁剪方括号中的内容。先看下面的例子:
> x=[[这是一个很长的字符串,而且我可以包括'和"]]
> print(x)
这是一个很长的字符串,而且我可以包括'和"
你还可以在两个方括号之间包括任意个等号——如果你的字符串中有“]]”,就象下面这个例子:
> x=[=[这是一个很长的字符串,它可以包括]],'和"]=]
> print(x)
这是一个很长的字符串,它可以包括]],'和"
你会很少用到这种形式,不过如果你想创建一个多行的字符串或字符串中含有大量引号时确实很有用。
2.4.6 转义特殊的字符
除了单引号和双引号字符以外,还有其他一些字符需要进行转换,例如回车换行。你可以通过使用“/n”来代表换行。
表2-4 字符转义表
转义字符 |
描述 |
/a |
响铃字符 |
/b |
退格符 |
/f |
换页符 |
/n |
换行符 |
/r |
回车符 |
/t |
水平制表符 |
/v |
竖直制表符 |
// |
反斜杠 |
/’ |
单引号 |
/” |
双引号 |
/xxx |
ASCII码为xxx的字符 |
在魔兽世界中,您一般只会遇到/n、//、/’、/”和/xxx这几个字符,因为魔兽世界的输出窗口并不支持其他几个。
2.4.7 获取字符串的长度
使用“#”运算符或使用string.len()函数都可以取得字符串的长度。
> print(#"Hello")
5
> print(string.len("This is a test string"))
21
> print(string.len("这是一个测试字符串"))
18
> foo="这是一个测试字符串"
> print(#foo)
18
string.len()之间的点意味着len()函数属于命名空间string的一部分(第4章会讲述有关命名空间的更多知识)。
默然说话:从上面的例子可以看出两件事情,第一,“#”和string.len()的运算结果相同,第二,一个中国字符在Lua中被看作是两个字符,所以“这是一个测试字符串”被认为有18个字符的长度,而不是习惯上的9个汉字,这在进行相关计算的时候需要注意,另外这一结果也暗示了,很可能在Lua中使用中国字符会存在比较麻烦的乱码问题。
2.5 布尔值和运算符
有3个逻辑运算符可以对布尔值使用:and、or和not,其实not的运算优先级别最高,and次之,or最低。因此为了你的运算结果正确,最好加上小括号。
2.5.1 使用and运算符
除了一般性的应用以外(当且仅当两个操作数都是true的时候,才返回true)。
> print(true and true)
true
> print(true and false)
false
> print(false and false)
false
> print(false and true)
false
这个运算符还有比较有趣的特性,正如下面的例子所示:
> print(true and 4)
4
> print(true and "Hello")
Hello
Lua的and逻辑运算同样遵循“短路运算”原则,所不同的是,如果第一个操作数是true,它只返回第二个操作数的值,如果第二个操作数的运算结果是一个非布尔值,则输出这个值。
2.5.2 or运算符的使用
只要两个操作数有一个为true,or运算符就返回true。
> print(true or true)
true
> print(true or false)
true
> print(false or true)
true
> print(false or false)
false
它同样也遵循“短路运算”原则,如果第一个操作数为false,它只返回第二个操作数的值,如果第二个操作数的运算结果是一个非布尔值,则输出这个值。
> print(false or 4)
4
> print(false or "Hello")
Hello
在C或Java中有一种问号运算符(a?b:c),在Lua中你可以使用逻辑运算符来模拟这个运算符:
> print(true and "Hello" or "艾泽拉斯")
Hello
> print(false and "Hello" or "艾泽拉斯")
艾泽拉斯
2.5.3 not运算符的使用
not就是取非运算,简单说,就是把布尔值取反。它是一个单目运算符。另外,在Lua中,所有不是false和nil的值都代表true。
> print(not false)
true
> print(not true)
false
> print(not 4)
false
> print(not "Hello")
false
> print(not nil)
true
2.5.4 理解nil值
nil是一个Lua中很特殊的值,表示什么也没有。在您使用变量和表(第5章将介绍)的时候会经常遇上它。
您可以用“==”运算符来测试一个值是否和nil等价,也可以查看一个值的类型来判定它是否为nil,象下面的例子那样。注意,由于type()函数总是返回一个字符串,所以在比较时需要使用字符串“nil”,而不是关键字nil。
> print(emptyVariable==nil)
true
> print(type(emptyVariable)=="nil")
true
2.6 探讨作用域
目前为止,您声明的所有变量都是全局变量,表明它在程序的任何地方都可以被访问。还有另一种变量,称为局部变量(默然说话:单词local直译的话是本地,所以有的编程书上又翻译为本地变量,它与局部变量是同一个单词的不同翻译。),它在程序其余部分的可见性可以用某些方法来限制。要完全明白全局变量和局部变量之间的差异,您必须了解块(Block)和组块(Chunk)的作用域。(默然说话:作用域又叫可见性规则)
2.6.1 块(Block)
将下面的语句输入Lua解释器中:
> do
>> local i=10
>> print("内部:" .. i)
>> end
内部:10
这里除了新的关键字do、end和local之外,我们只是简单地将10赋值给了i,然后将其作为字符串的一部分来显示。do关键字用来说明一个块的开始,而end则说明一个块的结束。接下来再试试这句:
> print(i)
nil
在do和end之间定义的变量,如果你增加了local关键字,它将成为了一个局部变量,只能在该do和end块之间访问,如果在外部(如我们上面的代码)是无法访问的,这就是我们会看到nil输出的原因。
除了do和end可以创建块,特定的Lua结构如for循环、while循环以及函数都会隐式地新建块。第3章将详细讨论。
默然说话:魔兽世界当中,我们写的插件会和别的许多插件共同工作,如果你使用了大量的全局变量,就会和其他插件产生严重冲突。因此,尽量使用局部变量吧。
2.6.2 组块(Chunk)
如果你注意的话,你应该在前面我们论述错误信息一节中的错误信息里看到了“Chunk”这个英文单词,它说明main组块(chunk)发生了错误。在Lua中,组块(chunk)要么是指正在执行的文件,要么是指正在运行的字符串。变量的作用域同样被限制在组块之中,这意味着你在一个文件里声明的局部变量是不能在另一个文件里访问的,因为它们在不同的组块之间。
在Lua解释器中,您输入的每一行都是它本身的组块,除非您将其封装在一个声中(如do..end块)。因此,下面的代码是无效的:
> local i=10
> print(i)
nil
这只是Lua解释器工作的独特方式。要避免这样的情况,可以用do..end来封装多行代码。
2.7 小结
本章简要介绍了Lua编程语言,包括变量、值、类型、运算符和作用域。第3~6章会详细介绍这种语言在魔兽世界中的应用。Lua编程语言在WoW以外也被广泛应用,同时也有许多好的书籍全面地介绍过这种语言。
第3章我们将开始使用函数来工作,作用域和可见性将变得非常重要。不过有一点必须要说明:除非特别指明,否则几乎所有的变量都是全局变量。