Skip to content

第二章 类型和值

​ Lua是动态类型语言,语言中不需要类型的声明,每个变量自身携带类型信息(变量类型自声明)。

Lua中有八种基本类型:

  • nil(空)
  • boolean(布尔)
  • number(数字)
  • string(字符串)
  • userdata(自定义类型)
  • function(函数)
  • thread(线程)
  • table(表)

"type"函数可以返回定值或变量的类型。

lua
print(type("Hello world")) --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type(X))) --> string

​ 最后一行打印的是"string"与"X"没有任何关系,因为"type"的返回值类型永远都是string。

​ 变量没有预定义的类型,任何变量都可以保存任何类型的值:

lua
print(type(a)) --> nil ('a' 没有初始化)
a = 10
print(type(a)) --> number(数字)
a = "a string!!"
print(type(a)) --> string (字符串)
a = print -- 这样是完全没有问题的!
a(type(a)) --> function(函数)

​ 注意最后两行:在Lua中,函数跟数字、字符串等一样是可以作为基本类型来进行操作修改的。

​ 通常把同一个变量赋值不同的类型会导致代码逻辑凌乱,但有时候清晰的使用这种特性也是相当有用。比如,"nil"可以作为正常返回值和不正常返回值的区分标志。

1 空值

​ 空值只有一个值就是"nil"。"nil"的主要用途是与其它类型进行区分。Lua使用"nil"来表示”没有值“这种情况,意味着当前缺少一个有效值。之前提到,一个全局变量在被赋值之前默认值就是"nil"。同样,可以把一个全局变量赋值为"nil"来删除它。

2.2 布尔型

​ 布尔型有两个值,"false"和"true",它们的意义与传统布尔型一致。但是在Lua中,并不是只能使用布尔型作为条件值,其它类型同样可以。在条件测试中(比如在if之类的控制结构中)布尔类型的"false"和"nil"都被认为是测试失败,其它值被认为是测试成功。特别需要注意的是,在Lua的条件测试中,数字0和空字符串都被认为是测试成功。

​ 贯穿全书,"false"意味着所有代表"失败"的值,包括"false "和"nil",如果想要单单强调布尔值,将会使用"false ";对于"true"和"true t "也做同样处理。

2.3 数字

​ Lua中没有整数类型,只要是数字就代表实数(双精度浮点型)。

​ 有人担心使用浮点会导致简单的递增或者比较都会出现偏差,但其实并非如此。事实上如今所有平台对于浮点运算的处理都遵循IEEE 754规范。根据这个规范,只有当数字在计算机中无法被准确表示的时候才有可能导致错误。某种操作只有在结果无法被准确表示的时候才会对结果进行修正,任何操作只要结果可以被准确表示,那么就必须真实的返回该结果。

​ 实际上用双精度浮点来表示整数,其最大值可以到2 53(近似10 16),在次之前的所有数都可以被准确的表示出来。当用双精度浮点来表示整数的时候,根本就不用担心修正错误,除非这个整数的绝对值大于2 53。需要指出的是,Lua中的数字可以准确的表示所有32位长的整形。

​ 当然,携带无穷位数小数部分的数字在用计算机进行计算时会出现精度错误,这种情况在用笔和纸进行运算的时候同样会出现。假设在纸上用小数来表示出1/7,我们必须取小数点后的某几位而不可能将它全部写出来。假设取10位精度,那么1/7就会被修正成0.142857142。如果用十位精度计算1/7*7,结果将是0.999999994,而不是1。此外,在十进制中可以用有限位数表示出来的数在二进制中有可能是无穷的:比如通过计算机的双精度计算12.7-20+7.3,结果并不是0,因为12.7和7.3无法使用有限位数的二进制来表示出来(详情参考练习2.3)。

​ 在继续学习下去前请牢记:整数一定可以被计算机准确的表示出来,并且不会出现修正错误。

​ 现代的大多数CPU处理浮点运算的速度同处理整数运算的速度相同,甚至更快。然而我们也可以重新编译Lua使它用其它类型来代表数字类型,比如长整形或单精度浮点,这在没有硬件级浮点运算支持的平台上很有用,比如嵌入式系统。可以阅读"luaconf.h"文件获取更多详情。

​ 可以使用科学计数法来表示一个数字常量,下面是一些合法的数字常量表示:

bash
4  0.4  4.57e-3  0.3e12  5E+20

​ 除此之外,在数字前加"0x"前缀就可以直接表示十六进制常量。Lua5.2之后,十六进制常量可以有小数部分和指数(用"p"或者"P"作为前缀),比如下面的例子:

bash
0xff(255) 0x1A3(419) 0x0.2(0x125) 0x1p-1(0.5)

0xa.bp2(42.75)

(括号中的值是该十六进制数的十进制表达形式)

2.4 字符串

​ Lua中的字符串概念通常表示连续的字符。Lua中的字符串完全是八位编码,其中可以包含任何数字编码,包括非字符串尾的0(在其它语言中,0往往只表示字符串的结尾,但Lua中不是这样)。这意味着可以把二进制数据存到一个字符串中,也可以把Unicode字符串用任何形式保存(UTF-8,UTF-16等),Lua自带的标准字符串库对这些表现形式没有做明确的支持。总是如此,在21.7节将会讨论到,使用UTF-8处理字符串更加合理。

​ Lua中的字符串值是不可变的,这意味着无法像在类似C语言中直接修改字符串中的某一个字符。作为Lua中的解决方案,对字符串内容进行修改的方法就是造一个新的字符串,就像下面的例子那样:

lua
a = "one string"
b = string.gsub(a, "one", "another") -- 修改字符串的部分内容
print(a) --> one string
print(b) --> another string

​ Lua中的字符串同Lua中的其它对象一样(表、函数等),托管于自动内存管理,这意味着不需要费心于字符串内存空间的申请和释放,Lua把这一切都处理好了。字符串可以仅仅包含一个字符,也可以包含一整本书,在Lua中编程操作包含十万甚至百万字符的字符串也是很常见的。

​ 可以在字符串前加"#"前缀(被称作长度运算符)来获取字符串的长度:

lua
a = "hello"
print(#a) --> 5
print(#"good\0bye") --> 8

字面字符串

可以使用单引号或者双引号来为一个字面字符串赋值:

lua
a = "a line"
b = 'another line'

​ 这两种方式是等价的,唯一的不同就是在每种引号的内部都可以使用另一种引号,而不需要转义。

​ 作为编程风格的统一要求,大多数程序员在同一个工程中都会使用单一类型的引号,而"类型"是应该取决于工程自身的内容的。比如一个处理XML的库可能就会使用单引号来引用一个XML文档结构,因为在XML文档结构中常常会包含双引号。

​ Lua中的字符串也具有跟C类似的转义字符:

bash
\a

蜂鸣

\b

退格

\f

换页

\n

换行

\r

回车

\t

水平tab

\v

垂直tab

\\

反斜杠

\"

双引号

\'

单引号

下面的例子可以阐述它们的使用方法:

lua
> 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: '\'

​ 可以使用十进制或十六进制的ASCII码来定义字符串中的字符,方法是""后面跟三位数的十进制ASCII码或者"\x"后面跟两位数的十六进制ASCII码。举一个稍微复杂的例子,下面的三个字符串具有相同的值:

lua
a="alo\n123\""
b='\97lo\10\04923"'
c='\x61\x6c\x6f\x0a\x31\x32\x33\x22'

​ 在b的赋值中,必须用"049"来表示字符"1",而不能将"0"省略。因为如果省略掉,Lua会按照"\492"、"\003"进行拆分解析,从而引发错误。

长字符串

​ 可以用被双方括号([[,]])括起来的文本给一个字符串赋值,用法跟注释很相似。括起来的内容可以包含多行,并且不会解释其中的转义字符。此外,如果文本中的第一个字符是换行符,还会被忽略。这种机制使得将包含多行的文本赋值给字符串非常方便,就像下面的例子:

lua
page = [[
<html>
<head>
	<title>An HTML Page</title>
</head>
<body>
<a href="http://www.lua.org">Lua</a>
</body>
</html>
]]

write(page)

有时候文本内容是一段代码,而代码中可能会出现像a=b[c[i]](注意代码中的]])嵌套的情况;或者有时候文本内容是一段包含注释(使用 [[ 和 ]] 进行注释)的代码。为了应对这些情况,可以在两个方括号中添加任意数量的等号,比如"[===["。这样修改右方括号之后,直到遇到另一个包含同样数量等号的左方括号才会认为是文本的终止(对应前面的例子应该是"]===]")。Lua会忽略掉包含等号数目不同的方括号对。通过选择合理数量的等号标志,就可以无需使用转义字符而嵌套字面字符串。

​ 在处理注释的时候,这个技巧同样适用:比如可以在一个以"--[=["作为起始、"]=]"作为结尾的长注释中嵌套包含注释的代码内容。

​ 使用长字符串在代码中插入文本内容是完美的方式,但依旧不建议在代码中将长字符串用于非文本内容使用:某些文本编辑器可能会处理出错;甚至在读取的时候,行尾结束标志"\r\n"会被换成"\n"。在代码中插入任意非文本内容的替代方案是使用数字转义,十进制或十六进制的均可,比如"\x13\x01\xA1\xBB"。但这再次引入一个问题:当内容较多时,全部写到一行中实在是太长了!

​ Lua5.2中引入一个新的转义字符"\z"来应对这种情况:它会跳过它和它之后的所有的空字符(比如换行、Tab、空格等)。下面的例子就是该转义字符的用法:

lua
data = "\x00\x01\x02\x03\x04\x05\x06\x07\z
\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"

​ 结尾的"\z"跳过了紧跟其后的换行和下一行的缩进,因此data字符串的最终内容是"0x07"之后紧跟的"0x08"。

强制转换

​ Lua提供了一套运行时自动转换字符串和数字的机制,任何算术运算中如果包含字符串,都会尝试将该

字符串转化为数字:

lua
print("10" + 1) --> 11
print("10 + 1") --> 10 + 1
print("-5.3e-10"*"2") --> -1.06e-09
print("hello" + 1) -- 错误(无法将"hello"转化为数字)

​ Lua不仅在算术运算符中会启用字符串到数字的强制转换,在一些其它期望得到数字的地方同样会触发该机制,比如字符串作为函数"math.sin"的参数。

​ 与此相反,当Lua期望得到字符串却得到数字的时候,它会将数字转换成字符串:

lua
print(10 .. 20) --> 1020

(".."是字符串连接符,当它紧跟随在一个数字后面的时候,必须使用空格将它与数字分隔开,否则Lua会认为第一个"."是小数点)

​ 到目前为止我们无法断定在Lua中启用自动强制转换是否是一个好想法 ,但作为忠告,最好不要依赖这种机制。少数地方它的确能够提供便利,但同样它也会增加Lua语言本身和编程过程的复杂度。尽管有自动转换,但字符串和数字本质上依旧是截然不同的东西。类似10=="10"这样的比较会返回false,因为10是一个数字,而"10"是一个字符串。

​ 如果想将字符串显式转换成数字,可以使用函数"tonumber",这个函数在字符串无法转换成数字的时候返回"nil":

lua
line = io.read() -- 从输入中读取字符串
n = tonumber(line)-- 尝试转化成数字
if n == nil then
	error(line .. " is not a valid number.")
else
	print(n*2)
end

如果想将数字转化成字符串,可以使用函数"tostring",或者将数字与一个空字符串连接:

lua
print(tostring(10) == "10") --> true
print(10 .. "" == "10") --> true

这些转换总是合法的。

5 表

​ Lua中的"表"本质是一个"字典"的实现。"字典"是一个数组,但它不仅仅可以像普通数组那样通过数字进行索引,还可以通过字符串或者其它任何Lua中的类型进行索引,除了nil。

​ 表是Lua中主要的(实际上也是唯一的)数据组织机制,功能十分强大。它使得我们可以通过简单、统一并且有效的方式去表示普通的数组、集合、记录和其它数据结构,Lua同样使用"表"来表示包和对象。从程序员的角度看,"io.read"意味着"io"模块的"read"方法,但在Lua看来,这个表达意味着从表"io"中用"read"键去查找其对应的值(此处值的类型是函数)。

​ Lua中的表既不是变量也不是值,它们是"对象"。对Java或者Scheme中"数组"概念熟悉的人会清晰的理解我要表达的意思。可以把一个表想象成一个对象的实例,程序操作的是它的引用(或者说指针)。Lua永远不会隐式的复制或者创建一个表,并且Lua中也不需要刻意声明一个表——实际上也没有方法供我们声明。通过使用构造器来创建一个表,最简单的形式就是用"{}"。

lua
a = {} -- 创建一个表,并将它的引用保存在a中
k = "x"
a[k] = 10 -- 添加新的条目,键名为"x",键值为10
a[20] = "great" -- 添加新的条目,键名为20 ,键值为"great"
print(a["x"]) --> 10
k = 20
print(a[k]) --> "great"
a["x"] = a["x"] + 1 -- 键"x"的值+1
print(a["x"]) --> 11

表都是匿名的,指向表的变量和表自身没有任何绑定关系:

lua
a = {}
a["x"] = 10
b = a -- b和a是同一个表的引用
print(b["x"]) --> 10
b["x"] = 20
print(a["x"]) --> 20
a = nil -- 只有b还指向该表
b = nil -- 该表没有被任何变量引用

​ 在程序中,当一个表失去了所有的引用,Lua的垃圾回收器便会自动删除这个表并回收它所使用的资源。

​ 每个表都可以保存各种各样的值,这些值可以有不同类型的索引,表会随着条目的增加调整自身的大小。

lua
a = {} -- 空表
-- 创建1000个新的条目
for i = 1, 1000 do
	a[i] = i*2
end
print(a[9]) --> 18
a["x"] = 10
print(a["x"]) --> 10
print(a["y"]) --> nil

​ 注意最后一行:跟全局变量类似,对表中未初始化的键取值,结果是nil。同样跟全局变量类似,可以把表中的某个键赋值为nil来删除它(注意"删除"这个字眼)。这并不是巧合,实际上Lua把全局变量保存在一个普通的表中。在第14章我们将会讨论相关内容。

​ 想要访问表中键必须使用"索引",除了按照a.["name"]的方式使用索引,Lua还支持a.name的形式(这种替代方式称为"语法糖")。因此,上面例子的最后几行可以用更加清晰、人性化的形式来表达:

lua
a.x = 10 -- 等价于 a["x"] = 10
print(a.x) -- 等价于 print(a["x"])
print(a.y) -- 等价于 print(a["y"])

​ 在Lua中,这两种形式完全等价,可以自由混搭使用。但作为代码的阅读"人",代码中出现这两种形式往往代表不同的意味:使用"."的形式访问表意味着表中已经存在多个固定的、预定义的键,表起到的作用是一个记录;使用另一种方式意味着表可以使用任何字符串作为键名对键值进行索引,并且因为某种原因正在操作当前键。

​ 初学者非常容易犯的一个错误是混淆a.x和a[x]。第一个形式表示a["x"],也就是说使用字符串"x"对表进行索引;第二个形式表示使用变量x的值对表进行索引,通过下面的例子可以看一下不同:

lua
a = {} x = "y"
a[x] = 10 -- 将a["y"]赋值为10
print(a[x]) --> 10 -- a["y"]的值
print(a.x) --> nil -- a["x"]未定义
print(a.y) --> 10 -- a["y"]的值

可以把表中所有值的索引全部设置为整数来模拟传统的数组或列表:在任何时候都没有一种方法也没有必要提前定义表的大小,只要按照需求初始化表中的元素就可以了。

lua
-- 从标准输入中读取10行并存储到表中
a = {}
for i = 1, 10 do
	a[i] = io.read()
end

​ 表可以使用任何类型的值作为索引,只要愿意,可以使用任何整数作为一个(表模拟的)数组的起始。在Lua中,普遍的做法是将"1"(不是"0",这点跟C语言等不同)作为起始,并且有一些机制依赖这儿约定。

​ 通常在操作列表之前,必须知道它的长度。它可能是一个固定的常量,也可能被保存在某个地方。我们习惯性的把列表的长度保存在一个非数字索引的位置,由于历史原因,一些程序使用键名"n"来存储。

​ 但多数时候长度是隐式的,在这种时候,可以使用"nil"值作为列表结尾的标志,因为对表中任何未初始化的位置进行索引结果都是nil。比如,当把10行内容读入到一个列表中的时候,很容易就知道这个列表的长度是10,因为它每个键名都是数字"1,2,……,10",这一点只有列表中没有"洞"的时候才有效:所谓"有洞"的意思是指在列表的两个非连续索引之间存在值为nil的索引。我们把没有洞的列表称作序列。

​ Lua对序列提供了长度操作符"#",它返回一个序列的最后索引值或者叫"长度"。比如,可以把上面例子中序列的内容全部打印出来:

lua
--打印a的内容
for i = 1, #a do
print(a[i])
end

​ 因为可以用任何值来对表进行索引,所以在使用"看起来相同"的值索引表的时候,结果却会产生差异。比如0和字符串"0"均可以作为表的索引,但这两个值是不同的,它们完全指向表中不同的位置。同样的,字符串"+1","01","1"也代表不同的位置。所以,在不确定索引的具体类型的时候,必须要用显示转换来明确它:

lua
i = 10; j = "10"; k = "+10"
a = {}
a[i] = "one value"
a[j] = "another value"
a[k] = "yet another value"
print(a[i]) --> one value
print(a[j]) --> another value
print(a[k]) --> yet another value
print(a[tonumber(j)]) --> one value
print(a[tonumber(k)]) --> one value

对这一点如果不保持警惕的话,那么就可能在程序中引入难以察觉的缺陷。

2.6 函数

​ 在Lua中,函数属于"第一类型":程序可以把函数保存到一个变量中,可以把函数作为参数传给另一个函数,也可以把函数作为返回值。这个设定能够给程序增加极大的灵活性:程序可以重载函数添加新的功能;或者为了营造一个安全的环境来运行不信任的代码(比如这个代码来源于互联网),仅仅只需要清除某个函数。此外,Lua为函数式编程也提供了良好的支持,比如在语法域允许的范围内进行函数嵌套使用。

第六章将会对有关"函数"的其它内容做一个详细说明。总而言之,将函数归于"第一类型"在Lua实现面向对象编程的过程中起到了关键作用。

2.7 自定义类型和线程

自定义类型允许将任意C语言数据存储到Lua变量中,但Lua对这种类型除了赋值和相等性校验,再没有其它预定义的操作。用户数据用于表示一个用C写的程序或库创建出来的新类型,例如标准I/O库就是使用该类型来表示打开的文件。稍后将在CAPI部分详细讨论自定义类型。第九章会解释线程类型,其中将会讨论协程。

练习

练习2.1:表达式type(nil)==nill的结果是什么(可以在Lua中验证结果)?对于这个结果该如何解释?

练习2.2:下面哪一个是可用的数字?他们的值分别是多少?

bash
.0e12 .e12 0.0e 0x12 0xABFG 0xA FFFF

0xFFFFFFFF 0x 0x1P10 0.1e1 0x0.1p1

练习2.3:小数12.7等于分数127/10,分母为10;加入分母规定为2,可以把它表示成一个正常的分数吗?5.5呢?

练习2.4:在Lua中,如何作为字符串嵌入下面的内容?使用最少两种方法。

lua
<![CDATA[
Hello world
]]>

练习2.5:在综合考虑可读性、单行最大长度和性能的情况下,怎么样把包含任意内容的二进制数据作为字面字符串嵌入到Lua中?

练习2.6:假设有如下代码:

lua
a={}; a.a=a

a.a.a.a的值是多少?其中的每个a与其它的a有什么不同?如果在上面的代码中加入下面一行:

lua
a.a.a.a=3

那么现在,a.a.a.a的值是多少?

Released under the MIT License.