楔子
我们这次来一起学习一下lua这门语言,因为python也可以调用lua。lua这门语言非常的简单,为什么说简单呢?首先lua的解释器是由C语言编写的,并且源码总共两万两千多行,可以说非常的少,就冲解释器的源代码数量,就决定了这门语言难不倒哪里去。
也正因为lua的精简性,导致它无法独立地开发大型应用,lua存在的意义主要是为了和其它语言结合,提供相应的扩展功能,比如在C++中引入lua脚本等等。
下面一起开始lua的学习之旅吧。
lua的安装
这里我们使用的是lua5.3,一个比较新的版本。直接去下载对应操作系统的lua即可,我这里使用的是Windows。lua下载我提供了百度网盘,可以直接下载:链接:https://pan.baidu.com/s/1UO2s_Xtof1XQIWJLP3cs2A 提取码:ne3q
下载之后是一个压缩包,解压之后就是图中显示的那样。我们看到,确实非常的精简,当然lua也提供了相应的标准库,不过在5.3版本中都嵌入到解释器里面了,也就是图中的lua53.dll。
将该目录设置到环境变量中,然后在终端中输入lua,即可进入到lua的交互式环境中。这里我就不演示了,我们下面会使用ide进行操作。关于ide,我这里使用的是pycharm,然后下载一个lua的插件即可。
下面老规矩,我们来打印一个hello world,这个仪式感不能丢。
print("hello world")
执行的时候,在控制台就会显示出"hello world"。估计有的小伙伴觉得,你这写的是python吧。其实,lua中的打印函数也是print,并且也是内置的,并且lua中的字符串可以使用单引号、也可以使用双引号,当然三引号不可以。
lua语言入门
任何一门语言都提供了不同类型的数据结构,那么lua中都有哪些数据结构呢?
nil:空
boolean:布尔类型,分别是true和false
number:数值型,整型和浮点型都属于number
string:字符串
table:表
function:函数
userdata:用户数据
thread:线程
lua总共提供了以上8中数据类型,目前只需要知道一下即可,我们先将一些基础概念等前置工作说一下,后面会一点一点地慢慢介绍。
lua中的关键字有22个
and、break、do、else、elseif、end、false、goto、for、function、if、in、local、nil、not、or、repeat、return、then、true、until、while,这些关键字不用刻意去记,当然既然学习lua,肯定有其它编程语言基础,所以这些关键字显然是大部分都见过的。至于不认识的关键字,我们后面也会慢慢遇到。
lua中的注释
lua中也分为单行注释,多行注释。
lua的单行注释和SQL一样,使用两个减号:--
。
-- 这是单行注释
lua的多行注释:以--[[
开头、]]
结尾,里面写注释。
--[[
这是多行注释
并且开头的--和[[之间不可以有空格,结尾是两个]
]]
下面这种写法也是多行注释,不是两行单行注释。
--[[
这也是多行注释
不是两行单行注释
--]]
lua中的数值
接下来让我们学习lua中的数据类型吧,首先是lua中的数值。
我们说lua中的数值类型为number,整型和浮点型都为number。
-- lua和python类似,在创建变量时不需要指定类型
-- 解释器会自动根据赋的值来判断
a = 123
b = 3.14
print(a, b) -- 123 3.14
-- lua中,每一行语句的结尾也不需要加分号,直接换行即可
-- 当然加分号也是可以的,跟python是类似的
c = 123;
d = 456
print(c, d) -- 123 456
-- 并且在python中,如果加上了分号,那么两行赋值可以写一行
-- 比如 e = 1; f = 2
-- 这在lua中也是可以的
e = 1; f = 2
print(e, f) -- 1 2
-- 但是lua比较彪悍的是,不加分号也可以
-- 如果在python中这么写,则肯定是报错的
g = 3 h = 4
print(g, h) -- 3 4
-- 但是我们不建议将多行赋值语句写在同一行里面,最好要分行写
-- 当然在lua中也可以使用多元赋值
a, b = 1, 2
print(a, b) -- 1 2
-- 这里可能有人发现了,我们在最上面已经创建了a和b这两个变量了
-- 但是我们下面又创建了,这一点和python类似,可以创建多个同名变量
-- 比如创建a = 1,然后又创建a = 2,这是允许的
-- 只不过这相当于发生了更新,将a的值由1变成了2,当然即便赋值为其它类型也是可以的
-- 比如我先创建a = 数值,然后再将a的值换成字符串也是可以的,这一点和python也是类似的
-- 因为在lua中,全局变量是通过table、也就是"表"来存储的
-- 这个table后面会详细说,你暂时可以理解为哈希表,或者当成python中的字典,而且python中全局变量也是通过字典存储的
我们通过lua中的数值型,来演示了lua中如何创建一个变量,并且还介绍了lua中全局变量的存储方式,下面再来看看如何区分整型和浮点
a = 123
b = 123. -- .后面不写东西的话,默认是.0
c = .123 -- .前面不写东西的话,默认是0.
print(a, b, c) -- 123 123.0 0.123
-- lua中,可以使用type函数检测变量的类型
print(type(a)) -- number
print(type(b)) -- number
print(type(c)) -- number
-- 这个type是内置的,它检测的是lua中的基础类型
-- 而我们说lua中不区分整型和浮点型,如果想精确区分的话,那么可以使用math.type
-- 整型是integer,浮点型是float
print(math.type(a)) -- integer
print(math.type(b)) -- float
print(math.type(c)) -- float
-- 如果一个数值中出现了小数点,那么math.type得到的就是float
-- 使用type和math.type得到的都是一个字符串
-- 另外,我们看到,我们是直接使用的math.type,这个math是哪里来的
-- 这类似于一个外部包,比如python中也有math
-- 只不过在lua中我们不需要导入,直接用即可,包括后面处理字符串用的包也是如此
整型和浮点之间的比较
print(3 == 3.0) -- true
print(-3 == -3.0) -- true
-- 我们看到,如果小数点后面是0,那么也是相等的,这一点和python也是一样的
-- lua中也支持如下方式
print(3e3) -- 3000.0
-- 但是我们看到,得到是浮点
-- 在lua中,只要十进制数中出现了小数点、或者出现了幂运算,那么得到的都是一个浮点
-- 准确的说,math.type检测的结果是float,因为lua中不区分整型和浮点,它们都是number
-- 这里我们说浮点只是为了方便,理解意思即可
-- lua中a ^ b表示a的b次方
-- 如果运算中出现了浮点数,或者发生了幂运算,那么结果就是浮点
print(3 ^ 2) -- 9.0
print(3 * 3) -- 9
lua中也支持16进制,以及一个数的取反操作
print(0x123) -- 291
print(0X123) -- 291
算术运算
算数运算没啥好说的,对于相加、相减、相乘、取模,如果是两个整型,那么结果还是整型,如果出现了浮点,那么结果为浮点。
print(1 + 2, 1 + 2.) -- 3 3.0
print(1 * 2, 1 * 2.) -- 2 2.0
print(1 - 2, 1 - 2.) -- -1 -1.0
print(13 % 5, 13. % 5) -- 3 3.0
如果相除,那么结果一定为浮点
print(3 / 2, 4 / 2, 4 / 2.) -- 1.5 2.0 2.0
当然在lua中,还有一个地板除,会对商向下取整,这是在lua5.3中引入的。
print(3 // 2, 4 // 2) -- 1 2
-- 另外,如果里面出现了浮点,那么即使是地板除,也一样会得到小数
print(4 // 2.) -- 2.0
-- 虽然是浮点,但我们看到的是1.0, 相当于还是有向下取整的效果
print(4 // 2.5) -- 1.0
当然我们说,lua中还有幂运算,使用^表示
print(3 ^ 4) -- 81.0
print(3e4) -- 30000.0
-- 我们说出现了幂运算,得到的一定是浮点
关系运算
关系运算符的话,大部分都是一样的。只有不等于,在其它编程语言中一般是!=
,但是在lua中是~=
位运算
位运算和主流编程语言也是比较类似,尤其是python,感觉lua的很多设计都和python比较相似
-- 按位与 &
print(15 & 20) -- 4
-- 按位或 |
print(15 | 20) -- 31
-- 按位异或 ~, 在python中是^,但我们说^在lua是位运算,lua中是 ~
print(15 ~ 20) -- 27
-- 取反, 取反的话也是 ~
print(~20) -- -21
-- 左移
print(2 << 3) -- 16
-- 右移
print(16 >> 2) -- 4
-- 以上这些操作符是在5.3当中才提供的,如果是之前的版本,则不能使用这些操作符
数学库
lua也提供了一个数学库,叫做math,里面定义了一些用于计算的函数,比如:sin、cos、tan、asin、floor、ceil。这个可以在用的时候,自己查看,这里就不演示了。
lua中的字符串
下面我们看看lua中的字符串,lua中字符串既可以使用双引号、也可以是但引号。注意:lua中的字符串是不可变量,不能本地修改,只能创建新的字符串。
name = "komeiji satori"
print(name) -- komeiji satori
-- 使用#可以获取其长度
print(#name, #"name") -- 14 4
-- 使用.. 可以将两个字符串连接起来
print("aa" .. "bb") -- aabb
print("name: " .. name) -- name: komeiji satori
-- ..的两边可以没有空格,但是为了规范,建议前后保留一个空格
-- ..前后还可以跟数字,会将数字转成字符串
print("abc" .. 3, 3 .. 4, 3 .. "abc") -- abc3 34 3abc
-- 另外如果是..的前面是数字的话,那么..的前面必须有空格
-- 也就是写成类似于 3 .. 的形式 不可以写 3..
-- 因为3后面如果直接出现了. 那么这个.会被当成小数点来解释
lua内部也支持多行字符串,使用[[
和]]
表示
msg = [[
你好呀
你在什么地方呀
你吃了吗
]]
print(msg)
--[[
你好呀
你在什么地方呀
你吃了吗
]]
字符串和数字的转换
lua中可以将字符串与数值相加,也可以相互转换
print("10" + 2) -- 12.0
-- 如果字符串和整型运算,那么得到的也是浮点
-- 你可以认为只有整型和整型运算才有可能得到整型,而字符串不是整型
-- 调用tonumber可以将字符串显式的转为整型
print(type(tonumber("10"))) -- number
print(tonumber("10") + 2) -- 12
-- 如果转化失败,那么结果为nil
print(tonumber("ff")) -- nil
-- 当然有些时候我们的数字未必是10进制,比如上面的ff,它可以是16进制
-- 如果需要进制,那么就给tonumber多传递一个参数即可
print(tonumber("ff", 16)) -- 255
print(tonumber("11101", 2)) -- 29
print(tonumber("777", 8)) -- 511
-- 8进制,允许出现的最大数是7,所以转化失败,结果为nil
print(tonumber("778", 8)) -- nil
-- 整型转化成字符串,则是tostring
print(tostring(100) == "100") -- true
print(tostring(100) == 100) -- false
-- 我们看到整型和字符串是可以相加的,当然相减也可以,会将字符串转成浮点
-- 也可以判断是否相等或者不相等,这个时候会根据类型判断,不会隐式转化了,由于两者类型不一样,直接不相等
-- 但是两者是无法比较大小的,只能判断是否相等或者不等
-- 为什么这么做,是因为 2 < 15 ,但是 "2" > "15"
-- 所以为了避免混淆,在比较的时候lua不会隐式转化、加上类型不同也无法比较大小,因此直接抛异常
字符串标准库
lua处理字符串还可以使用一个叫做string的标准库,这个标准库在5.3中也是内嵌在解释器里面的,我们直接通过string.xxx即可使用。下面就来看看string这个标准库都提供了哪些函数吧,这里说一下lua中的字符串是以字节为单位的,不是以字符为单位的,因此string的大部分函数不适合处理中文(也有少数例外),如果是中文,可以使用后面要介绍的utf8
-- 查看字符串的长度
print(string.len("abc"), #"abc") -- 3 3
-- 一个汉字占三个字节,默认是以字节为单位的,计算的是字节的个数
print(string.len("古明地觉"), #"古明地觉") -- 12 12
-- 重复字符串n次
print(string.rep("abc", 3)) -- abcabcabc
-- 如果是单纯的重复字符串的话,也可以对中文操作,因为不涉及字符的局部截取
print(string.rep("古明地觉", 3)) -- 古明地觉古明地觉古明地觉
-- 字符串变小写,可以用于中文,但是没意义
print(string.lower("aBc")) -- abc
print(string.lower("古明地觉")) -- 古明地觉
-- 同理还有转大写
print(string.upper("aBc")) -- ABC
print(string.upper("古明地觉")) -- 古明地觉
-- 字符串翻转,这个不适合中文
print(string.reverse("abc")) -- cba
print(string.reverse("古明地觉")) -- ��谜厘椏�
-- 我们看到中文出现了乱码,原因就是这个翻转是以字节为单位从后向前翻转
-- 而汉字占3个字节,需要以3个字节为单位翻转
-- 字符串截取,注意:lua中索引是从1开始的
-- 结尾也可以写成-1,并且字符串截取包含首位两端
print(string.sub("abcd", 1, -1)) -- abcd
print(string.sub("abcd", 2, -2)) -- bc
-- 可以只指定开头,不指定结尾,但是不可以开头结尾都不指定
print(string.sub("abcd", 2)) -- bcd
-- 同样不适合中文,除非你能准确计算字节的数量
print(string.sub("古明地觉", 1, 3)) -- 古
print(string.sub("古明地觉", 1, 4)) -- 古�
-- 超出范围,就为空字符串
print(string.sub("古明地觉", 100, 400) == "") -- true
-- 将数字转成字符
print(string.char(97)) -- a
-- 如果是多个数字,那么在转化成字符之后会自动拼接成字符串
print(string.char(97, 98, 99)) -- abc
-- 字符转成数字
-- 默认只转化第1个
print(string.byte("abc")) -- 97
-- 可以手动指定转化第几个字符
print(string.byte("abc", 2)) -- 98
print(string.byte("abc", -1)) -- 99
-- 超出范围,那么返回nil
print(string.byte("abc", 10) == nil) -- nil
-- 转化多个字符也是可以的,这里转化1到-1之间的所有字符
print(string.byte("abc", 1, -1)) -- 97 98 99
-- 越界也没事,有几个就转化几个
print(string.byte("abc", 1, 10)) -- 97 98 99
-- 另外,这里是返回了多个值,我们也可以用多个变量去接收
a, b, c = string.byte("abc", 1, 10)
print(a, b, c) -- 97 98 99
-- 关乎lua返回值,这涉及到了函数,我们后面会说
-- 字符串的格式化,格式化的风格类似于C
print(string.format("name = %s, age = %d, number = %03d", "古明地觉", 17, 1)) -- name = 古明地觉, age = 17, number = 001
-- 字符串的查找,会返回两个值,分别是开始位置和结束位置
print(string.find("abcdef", "de")) -- 4 5
-- 不存在则为nil
print(string.find("abcdef", "xx")) -- nil
-- 字符串的全局替换,这个替换可以用中文
-- 返回替换之后的字符串和替换的个数
print(string.gsub("古名地觉 名名 那么可爱", "名", "明")) -- 古明地觉 明明 那么可爱 3
-- 我们同样可以使用返回值去接
new_str, count = string.gsub("古名地觉 名名 那么可爱", "名", "明")
print(new_str) -- 古明地觉 明明 那么可爱
关于处理ASCII字符,string库为我们提供了以上的支持,我们看到支持的东西还是比较少的,因为lua的源码总共才两万两千多行,所以这就决定了它没办法提供过多的功能。lua主要还是用来和别的语言结合的,然而事实上,string库提供的东西也不少了。
下面来看看utf-8,我们说string库不是用来处理unicode字符的,如果处理unicode字符的话,需要使用utf8这个库
-- lua中存储unicode字符使用的编码是utf-8
-- 计算长度
print(utf8.len("古明地觉")) -- 4
-- 类似于string.byte,这两个可以通用
print(utf8.codepoint("古明地觉", 1, -1)) -- 21476 26126 22320 35273
-- 类似于string.char,这两个可以通用
print(utf8.char(21476, 26126, 22320, 35273)) -- 古明地觉
-- 截取,使用string.sub
-- 可以使用utf-8编码,不同字符占的字节大小可能不一样,这时候怎么截取呢
-- 使用utf8.offset计算出,偏移到第n个字符的字节量
print(string.sub("古明地觉", utf8.offset("古明地觉", 2))) -- 明地觉
print(string.sub("古明地觉", utf8.offset("古明地觉", -2))) -- 地觉
-- 遍历,遍历使用了for循环,我们后面说,现在先看一下
for i, c in utf8.codes("古a明b地c觉") do
print(i, c, utf8.char(c))
--[[
1 21476 古
4 97 a
5 26126 明
8 98 b
9 22320 地
12 99 c
13 35273 觉
]]
end
以上便是lua处理字符串的一些操作, 尽管功能提供的不是非常的全面,但这与lua的定位有关。
lua的控制结构
本来是介绍lua中的数据类型的,但是由于里面很多特性都用到了循环,所以先来介绍一下lua的控制语句吧。
条件语句
我们来看看lua中的条件语句怎么写,lua中条件语句可以其它编程语言类似,单独的if、if和else、if...elseif...elseif...else。
-- 单独的if
if condition then
statement
end
-- if和else
if condition then
statement
else
statement
end
-- if..elseif...else
-- 注意:是elseif,不是else if,else和if之间需要连起来
-- 可以写多个elseif,但是只能有至多一个if和一个else,当然else也可以没有,但if必须有
if condition then
statement
elseif condition then
statement
elseif condition then
statement
else
statement
end
if和elseif后面必须加上一个then,类似于python中必须加上一个冒号一样,但是else则不需要then,另外每个if语句,在结尾处必须有一个end,来标志这个if语句块的结束。
既然结尾处有end,那么lua中也就不需要缩进了。但是python中是必须严格按照缩进来的,而对于lua则不被缩进约束,但还是那句话,为了代码的可读性还是建议按照python的规范来编写
a = 85
if a > 80 then
print("a > 80")
end
-- a > 80
if a > 85 then
print("a > 85")
else
print("a <= 85")
end
-- a <= 85
if a < 60 then
print("不及格")
elseif a < 85 then
print("及格")
elseif a < 100 then
print("优秀")
elseif a == 100 then
print("满分")
else
print("无效的分数")
end
-- 优秀
我们说,lua中的if不受缩进影响,那么有时候单个if我们也可以写在一行
a = 85
-- 这么写也是可以的
if a > 80 then print("a > 80") end
-- if 和 else也可以写在一行
if a > 85 then print("a > 85") else print("a <= 85") end
-- 甚至if elseif else也可以写在一行,或者自由换行也是可以的
if a < 60 then
print("不及格") elseif a
< 85 then print("及格") elseif
a <= 100 then print("优秀") else
print("无效的分数") end
-- 对于if elseif else,最好不要像上面那么写,尽管它是支持的,但是不要这么做
-- 由此我们看到,lua不是靠缩进来规范语句的,而是靠关键字
嵌套if也是类似的
age = 18
gender = "female"
if gender == "female" then
if age >= 18 then
print("或许我们之间可以做一场交易")
else
print("国家有一套完整的刑法")
end
else
if age >= 18 then
print("女装好看的话也是可以的")
else
print("溜了溜了")
end
end
-- 或许我们之间可以做一场交易
上面的嵌套if是标准写法,但是我们说lua中的if不受缩进的约束
age = 18
gender = "female"
if gender == "female" then
if age >= 18 then
print("或许我们之间可以做一场交易")
else print("国家有一套完整的刑法")
end
else
if age >= 18 then
print("女装好看的话也是可以的")
else
print("溜了溜了")
end end
-- 或许我们之间可以做一场交易
所以我们不考虑缩进也是可以的,但是不要这么写,一定要按照规范来写。如果是python的话,要是这样不管缩进,直接就报错了。
循环while语句
循环while语句,格式如下。
while condition do
statement
end
举个例子
i =
1 sum = 0
while i < 10 do
sum = sum + i
i = i + 1
end
print(string.format("sum = %d", sum)) -- sum = 45
repeat ... until
repeat ... until说白点,就是一直重复做,直到满足某个条件停下来。
i =
1 sum = 0
-- 不断的执行 sum = sum + 1 i = i + 1,直到满足 i >= 10的时候停下来
repeat
sum = sum + i
i = i + 1
until i >= 10
print(string.format("sum = %d", sum)) -- sum = 45
这里忘记提了,我们这里的条件只有一个,如果是多个条件呢?和python一样,使用and、or
age = 18
gender = "female"
if age == 18 and gender == "female" then
print("或许我们之间可以进行一场交易")
else
print("抱歉,打扰了")
end
-- 或许我们之间可以进行一场交易
a = 12
b = 5
if a > 12 or b <= 5 then
print("~~~")
else
end
-- ~~~
-- 从上面我们可以看到,语句块里面可以不写任何东西,如果是python的话,则是需要使用pass作为占位符
-- and是左右两边都成立,整体才成立,而or则是两边有一个成立,整体就成立
-- 当然lua中还有not表示取反,得到布尔值
-- 这里着重强调一点,在lua中只有false和nil才为假,其它全部为真
-- 这里和python不一样,在python中0、""是假,但在lua中是真
-- 再次强调,lua中只有false和nil才是假
print(not 0) -- false
print(not "") -- false
print(not not "") -- true
-- 0和""为真,所以使用not得到假,两个not得到真
循环for语句
for语句分为两种,我们来看一下
for a = 1, 8 do
print(a)
end
--[[
1
2
3
4
5
6
7
8
]]
-- 我们看到循环打印了1到8
-- 当然还可以跟上步长
for a = 1, 8, 3 do
print(a)
end
--[[
1
4
7
]]
上面是简单的遍历数字,for循环还可以遍历表,等介绍表的时候我们再说。
break
break则是跳出循环体,可以用于for、while、repeat,注意:没有continue
for a = 1, 8 do
print(a)
break
end
-- 1
-- 比如,如果a是偶数,那么我们就继续下一层循环
-- 但是lua中没有continue,我们可以是if else进行模拟
for a = 1, 8 do
if a % 2 == 0 then
else print(a) end
end
--[[
1
3
5
7
]]
lua中的表
下面我们来看看lua中的表(Table),表是lua语言中最主要(事实上也是唯一)的数据结构,lua中的表既可以当做数组来用,也可以当成哈希表来用。这个和python中的字典非常类似,比如:我们之前用查看变量类型的math.type,本质上就是以字符串"type"来检索表math。而在python中,比如调用math.sin,本质也是从math模块的属性字典里面查找key为"sin"对应的value。
# python代码
import math
print(math.sin(math.pi / 2)) # 1.0
print(math.__dict__["sin"](math.pi / 2)) # 1.0
然后看看在lua中如何创建表
-- 类似于python中的字典,lua中创建表直接使用大括号即可
t = {}
-- 返回的是表的一个引用
print(t) -- table: 00000000010b9160
-- 类型为table
print(type(t) == "table") -- true
在这里我们需要介绍一下lua中的变量,当然lua中分为全局变量和局部变量,这两者我们会在函数中细说。总之,我们目前创建的都是全局变量,其有一个特点:
-- 对于没有创建的变量,可以直接打印,结果是一个nil
print(a) -- nil
-- c这个变量没有创建,因此是nil,那么d也是nil
d = c
print(d) -- nil
-- 所以我们看到程序中,明明没有这个变量,但是却可以使用,只不过结果为nil
-- 那么如果我们将一个已经存在的变量赋值为nil,是不是等于没有创建这个变量呢?
-- 答案是正确的,如果将一个变量赋值为nil,那么代表这个变量对应的内存就会被回收
name = "shiina mashiro"
name = nil -- "shiina mashiro"这个字符串会被回收
之所以介绍全局变量这个特性,是因为在我们的表中,nil是一个大坑,我们往下看。
a = {}
a["name"] = "古明地觉"
a["age"] = 16
-- 打印a只是返回一个引用
print(a) -- table: 00000000000290e0
print(a["name"], a["age"]) -- 古明地觉 16
-- 更改表的元素
-- table类似于哈希表,key是不重复的,所以重复赋值相当于更新
a["age"] = a["age"] + 1
print(a["age"]) -- 17
-- 还记得我们之前说,全局变量也是通过table存储的吗
-- 我们可以一个变量不断地赋值,赋上不同类型的值
-- 就是因为table对应value没有限制
-- 可以附上任意类型的value,相当于发生了更新操作
a["age"] = 18
print(a["age"]) -- 18
a["age"] = "十六"
print(a["age"]) -- 十六
-- 创建table返回的是一个引用
b = a
-- 此时的b和a指向的是同一个table,修改b会影响到a
b["name"] = "satori"
print(a["name"]) -- satori
-- 我们说赋值给nil,等价于回收对象
a = nil
-- 但是只将a赋值为nil,显然还不够,因为还有b在指向上面的table
b = nil
-- 这样的话,table就被回收了
我们说lua中的table既可以做哈希表,也可以当做数组,有兴趣可以看lua的原代码,非常的精简。下面我们来看看table如何当成数组来使用:
a = {}
for i = 1, 10 do
a[i] = i * 2
end
print(a[3]) -- 6
-- table中如果key是整型,那么会通过数组的方式来存储
-- table在C中是一个结构体,里面实现了哈希表和数组两种结构
-- 如果key是整型,那么会通过数组的方式来存储,如果不是,会当成哈希表来存储
-- 注意:如果当成数组使用,那么索引也是从1开始的。
-- 此时是通过哈希表存储的
a["x"] = 233
print(a["x"]) -- 233
-- 除了a["x"]这种方式,还可以使用a.x,这两者在lua中是等价的
print(a.x) -- 233
-- a["name"]和a.name是等价的,但是和a[name]不是等价的,因为name是一个变量,而name = "x",所以结果是a["x"]或者a.x
a["name"] = "椎名真白"
name = "x"
print(a["name"], a.name, a[name]) -- 椎名真白 椎名真白 233
我们说2和2.0是相等的,所以在table中是怎么样表现的呢?
a = {}
a[2] = 123
print(a[2.0]) -- 123
a[2.0] = 456
print(a[2]) -- 456
-- 所以这两者是等价的,因为2.0会被隐式转化为2
-- 事实上这在python的字典中也有类似的现象
-- d = {}; d[True] = 1; d[1] = 2; d[1.0] = 3; print(d)
-- 上面那行代码在python里面执行一下,看看会发生什么
-- 但是对于字符串则不一样的
a = {}
a[2] = 123
a["2"] = 456
print(a[2], a["2"]) -- 123 456
-- 如果访问表中一个不存在的key呢?
print(a["xxx"]) -- nil
-- 我们看到得到的是一个nil
-- 显然我们想到了,如果将一个key对应的值显式的赋值为nil,那么也等价于删除这个元素
a[2] = nil
表构造器
估计有人目前对table即可以当数组又可以当哈希表会感到困惑,别着急我们会慢慢说。
我们目前创建表的时候,都是创建了一张空表,其实在创建的时候是可以指定元素的。
a = {"a", "b", "c" }
print(a[1], a[2], a[3]) -- a b c
-- 我们看到,我们没有指定key,所以此时表里面的三个元素是通过数组存储的,这种存储方式叫做"列表式(list-style)"
-- 索引默认是1 2 3 4...
-- 此外,还可以这么创建
b = {name="mashiro", age=18 }
print(b["name"], b["age"]) -- mashiro 18
-- 第二种方式是通过哈希表存储的,这种存储方式叫做"记录式(record-style)"
-- 此时的"name"、"age"我个人习惯称之为key
-- 因为本人是做python的,就按照python中的dict就是由key: value组成的
-- 当然你也可以称之为索引
-- 但如果我们存储的key是数字或者说特殊字符呢?答案是使用[]包起来
b = {["+"]="add", [3] = "xxx"} -- 必须使用 ["+"]=和[3]= 不能是单独的 +=和 3=
-- 同理获取也只能是b["+"]和b[3],不可以是b.+和b.3
print(b["+"], b[3]) -- add xxx
-- 表也是可以嵌套的
a["table"] = b
print(a["table"]["+"]) -- add
-- 此外,两种方式也可以混合使用
mix = {\'a\', name=\'mashiro\', \'b\', age=18 }
print(mix[1], mix[2]) -- a b
print(mix["name"], mix["age"]) -- mashiro 18
-- 这里有必要详细说明一下,即使是混合使用
-- 如果没有显式地指定key、也就是列表式,那么会以数组的形式存储,索引默认是1 2 3...
-- 所以a[1]是\'a\', a[2]是\'b\'
-- 如果是这种情况呢?
mix = {\'a\', [2] = 1 }
print(mix[2]) -- 1
mix = {\'a\', \'b\', [2] = 1 }
print(mix[2]) -- b
-- 解释一下,首先啊对于单个标量来说,默认就是用数组存储的,索引就是 1 2 3...
-- 但是我们在通过记录式设置的时候,对应的key使用的如果也是数组的索引
-- 那么记录式中设置的值会被顶掉
--[[
比如:mix = {\'a\', [2] = 1 }, 数组的最大索引是1,所以[2] = 1是没有问题的
但是mix = {\'a\', \'b\', [2] = 1 },数组最大索引是2,所以[2] = 1会被顶掉,因为冲突了
]]
-- 事实上
-- mix = {\'a\', \'b\', [2] = 1 }这种方式就等价于mix = {[1] = \'a\', [2] = \'b\', [2] = 1 }
-- 如果key是整型,那么通过数组存储, 否则通过哈希表存储
-- 只不过我们手动指定[2] = 1会先创建,然后被[2] = \'b\'顶掉罢了
a = {\'a\', [1] = 1 }
print(a[1]) -- \'a\'
a = {[1] = 1, \'a\'}
print(a[1]) -- \'a\'
-- 不管顺序,a[1]都会是\'a\'
估计有人还有疑问,那就是a = {}; a[1] = 1; a[100] = 100
或者a = {1, [100] = 100}
,如果这样创建的话,那么中间的元素是什么?因为我们说key是整型是以数组存储的,而数组又是连续的存储的空间,而我们只创建了两个元素,索引分别是1和100,那么其它元素是以什么形式存在呢?带着这些疑问,我们先往下看。
数组、列表和序列
现在我们知道了如果想表示常见的数组、或者列表(数组不用说,列表是python中的,都是一段连续的存储空间),那么只需要使用整型作为索引即可。
而且在lua的table中,可以使用任意数字作为索引,只不过默认是从1开始的,lua中很多其他机制也遵循此惯例。
但是table的长度怎么算呢?我们知道对字符串可以使用#,同理对table也是如此。
a = {1, 2, 3, name = \'mashiro\', \'a\' }
print(#a) -- 4
-- 但是我们看到,结果为4,可明明里面有5个元素啊
-- 因为#计算的是索引为整型的元素的个数,更准确的说#计算的是使用数组存储的元素的个数
a = {[0] = 1, 2, 3, 4, [-1]=5}
print(#a) -- 3
-- 此时的结果是3,因为0和-1虽然是整型,但它们并没有存储在数组里
-- 因为lua索引默认是从1开始,如果想要被存储的数组里面,那么索引必须大于0
a = {1, 2, [3.0]="xxx", [4.1] = "aaa" }
print(#a) -- 3
-- 这里同样是3,因为3.0会被隐式转化为3,因此数组里面有3个元素,但是4.1不会
所以我们看到,#计算的是存储在数组里面的元素,也就是table中索引为正整型的元素,但真的是这样吗?
首先对于数组中存在空(nil)的table,#获取长度是不可靠的,它只适用于数组中所有元素都不为nil的table。事实上,将#应用于table获取长度一直是饱受争议,以前很多人建议如果数组中存在nil,那么使用#操作符直接抛出异常,或者说扩展一下#的语义。然而,这些建议都是说起来容易做起来难,主要是在lua中数组实际上是一个table,而table的长度不是很好理解。
我们举例说明:
a = {1, 2, 3, 4 }
a[2] = nil
print(#a) -- 4
-- 上面我们很容易得出这是一个长度为4,第二个元素为nil的table
-- 但是下面这个例子呢? 没错,就是我们之前说的
b = {}
b[1] = 1
b[100] = 100
-- 是否应该认为这是一个具有100个元素,98个元素为nil的table呢?
-- 如果我们再将a[100]设置成nil呢,该列表长度又是多少呢?是100、99还是1呢
print(#b) -- 1
-- lua作者的想法是,像C语言使用\0作为字符串的结束一样,lua中可以使用nil来隐式地表示table的结束
-- 所以此时的结果是1
-- 但是a的第二个元素也是nil啊,为什么是4呢,别急往下看
c = {1, nil, 3, 4}
print(#c) -- 4
-- 啊嘞嘞,咋变成4了,难道不是1吗?别急,继续看
d = {1, nil, 3, 4, nil }
print(#d) -- 1
-- 我屮艸芔茻,为啥又变成1了。
-- 如果在table中出现了nil,那么#的结果是不可控的
-- 有可能你多加一个nil,结果就变了。当然,不要去探究它的规律,因为这没有意义
-- 总之不要在table中写nil,在table中写nil是原罪。不管是列表式、还是记录式,都不要写nil,因为设置为nil,就表示删除这个元素
-- 回到b这个table中
-- 我们说它的长度为1
print(#b) -- 1
-- 但是数组中确实存在索引为100的元素
print(b[100]) -- 100
-- 所以对b这个table,其中数组到底是怎么存储的,其实没必要纠结
-- 就当成索引为2到索引为99的元素全部是nil即可,但是计算长度的时候是不准的
-- 总之table中最好不要出现nil
遍历表
我们可以使用for循环去遍历table
a = {"a", "b", name="mashiro", "c", age=18, "d" }
-- for循环除了for i = start, end, step这种方式之外,还可以作用在表上面
-- 只不过需要使用pairs将table包起来,for k, v in pairs(t)
for index, value in pairs(a) do
print(index, value)
--[[
1 a
2 b
3 c
4 d
age 18
name mashiro
]]
end
-- 这里for循环中出现了两个循环变量,分别表示索引和值
-- 如果只有一个变量,那么得到的是索引,或者哈希表的key
-- 我们name和age好像顺序不对啊,是的,因为是通过哈希表存储的,所以不保证顺序
-- 但是对于数组来说,则是按照索引从小到大的方式存储、并输出的
-- 除了pairs,还有ipairs
-- ipars是只遍历存在于数组里面的元素
a = {[4] = "a", [3] = "b", name="mashiro", [1] = "c", age=18, [2] = "d" }
for index, value in ipairs(a) do
print(index, value)
--[[
1 c
2 d
3 b
4 a
]]
end
-- 打印按照索引从小到大打印,但是不建议这么创建table
如果table中出现了nil,那么我们使用for循环去遍历会发生什么奇特的现象呢?
-- 不过在此之前,还是先来看看一个坑向的
a = {[3] = 1, \'a\', \'b\', \'c\' }
-- 这个时候a[3]是多少呢?
print(a[3]) -- c
-- 我们说只要是列表式,都是从1开始,所以[3] = 1最终会被[3] = \'c\'所顶掉
-- 下面我们来看看table中出现了nil,for循环会如何表现
a = {\'a\', nil, \'b\', \'c\' }
print(#a) -- 4
for index, value in ipairs(a) do
print(index, value)
--[[
1 a
]]
end
-- 长度虽然是4(当然我们知道这不准),但是在遍历的时候一旦遇到nil就会终止遍历。当然这个nil要是数组中的nil,不是哈希表中的nil
-- 但如果是pairs,那么会遍历值不为nil的所有记录
a = {\'a\', nil, \'b\', \'c\', name=nil, age=18}
for index, value in pairs(a) do
print(index, value)
--[[
1 a
3 b
4 c
age 18
]]
end
-- 但是我们看到值"b"对应的索引是3,尽管前面的是nil,但是毕竟占了一个坑
-- 所以"b"对应的索引是3
-- 当然我们还可以使用获取长度、数值遍历的方式,当然前提是table中不能出现nil
a = {\'a\', \'b\', 123, \'xx\' }
for idx = 1, #a do
print(a[idx])
--[[
a
b
123
xx
]]
end
表标准库
表的标准库提供一些函数,用于对表进行操作,注意:这个标准库也叫table。
a = {10, 20, 30 }
print(a[1], a[2], a[3]) -- 10 20 30
-- 使用table.insert可以插入一个值
-- 接收参数为:table 插入位置 插入的值
table.insert(a, 2, "xxx")
print(a[1], a[2], a[3], a[4]) -- 10 xxx 20 30
-- 如果不指定位置,那么默认会添加在结尾
-- 此时传递两个参数即可:table 插入的值
table.insert(a, "古明地觉")
print(a[#a]) -- 古明地觉
-- 既然有insert,那么就会有remove
-- 接收参数:table 移除的元素的位置(索引)
print(a[1], a[2], a[3], a[4], a[5]) -- 10 xxx 20 30
table.remove(a, 3)
print(a[1], a[2], a[3], a[4], a[5]) -- 10 xxx 30 古明地觉 nil
-- 我们看到使用remove之后,后面的元素会依次向前移动
-- 因此无需担心会出现nil什么的
-- 不过这也说明了,remove的效率不是很高,因为设置到元素的移动
-- 但是table中的函数都是C实现的,也是很快的,因此也不用太担心
-- 另外,在lua5.3中,还提供了一个move函数
-- table.move(table, start, end, target),表示将table中[start, end]之间的元素移动到索引为target的位置上
-- 也是start位置的元素跑到target上面,start + 1 -> target + 1、 end -> target + end - start
t = {1, 2, 3, 4}
table.move(t, 2, #t, 3)
print(t[1], t[2], t[3], t[4], t[5]) -- 1 2 2 3 4
-- 很好理解,{1 2 3 4}中索引为[2, #t],移动到索引为3的位置上,因此结果是1 2 2 3 4,结果会多出一个
-- 这里的move实际上是将一个值从一个地方拷贝 copy 到另一个地方
-- 另外,我们除了可以将元素移动到table本身之外,还可以移动到另一个table
t1 = {"a", "b", "c", "d" }
t2 = {"x", "y" }
-- 表示将t1中[2, #t1]的元素移动到t2中索引为2的地方
table.move(t1, 2, #t1, 2, t2)
for idx = 1, #t2 do
print(t2[idx])
--[[
x
b
c
d
]]
end
-- table标准库中还提供了concat函数,会将表里面的元素拼接起来
a = {1, 2, "xxx", 3, "aaa" }
print(table.concat(a)) -- 12xxx3aaa
来个思考题吧
a = "b"
b = "a"
t = {a = "b", [a] = b }
print(t.a, t[a], t[t.b], t[t[b]])
-- 上面的print会打印出什么呢?我们分析一下
-- 首先看t这个表,其中a = "b"无需多说
-- 关键是[a] = b,我们说a和b都是变量,并且a = "b" b = "a", 所以结果等价于["b"] = "a", 即:b = "a"
-- 因此这里的t可以看做是 {a = "b", b = "a"}
-- 那么t.a显然是"b"
-- t[a]等于t["b"],因此结果是"a"
-- t.b结果是"a",那么t[t.b]等于是t["a"],所以结果是"b"
-- t[b] -> t["a"] -> "b",那么t[t[b]] -> t["b"] -> "a",因此结果是"a"
-- 所以print会打印出: "b" "a" "b" "a"
-- 下个问题
a = {}
a.a = a
print(a) -- table: 0000000000d98ef0
print(a.a) -- table: 0000000000d98ef0
print(a.a.a) -- table: 0000000000d98ef0
-- 我们发现打印的都是一样的,我们说lua中的table返回的一个引用
-- a.a = a,本身显然陷入了套娃的状态
lua中的函数
下面我们来介绍一下lua中的函数,lua的函数可以说是非常的有意思,尤其是它的参数和返回值的设定很有趣,不过在介绍之前,我们需要来说一下lua中的全局变量和局部变量。
-- 我们直接创建的变量,默认是全局的,在哪里都可以使用
-- 如果想创建一个局部变量,那么需要使用local关键字
-- 这样创建的变量就只能在对应的作用域中生效
if 2 > 1 then a = 123 end
print(a) -- 123
-- 当上面的if语句执行完之后,a这个变量就被创建了
if 2 > 1 then local b = 123 end
print(b) -- nil
-- 我们看到此时打印的是nil,因为上面if语句中的变量b,我们使用local关键字
-- 代表它是一个局部的,只能在对应的if语句中使用,外面没有b这个变量,所以打印结果为nil
name = "mashiro"
if 2 > 1 then local name = "satori" end
print(name) -- mashiro
if 2 > 1 then name = "satori" end
print(name) -- satori
-- 如果是local,那么相当于创建了新的局部变量,if里面的name和外部的name是不同的name
-- 但如果没有local,那么创建的都是全局变量,而外面已经存在name,因此相当于直接对外部的name进行修改
for i = 1, 10 do
end
-- 不仅是if,for循环也是如此,里面如果使用了local关键字创建的变量,那么外部也是无法使用的
-- 这里我们看一下循环变量i, 我们发现变量i在循环结束之后也不能使用了,当然python中是可以的
print(i) -- nil
i = 0
for i = 1, 10 do
end
print(i) -- 0
-- 我们看到打印的是0,说明for循环的i和外部的i是没有关系的
-- 不仅是for循环,while循环也是如此
-- 还有repeat ... until
i = 1 sum = 0
repeat
sum = sum + i
-- 尽管x是局部变量但是它对until是可见的
local x = sum
until x > 30
-- 再比如函数,还没有介绍,但是可以先感受一下
function add()
b = "aaa"
end
print(b) -- nil
add()
print(b) -- aaa
-- 当我们直接print(b)结果为nil
-- 但是当执行了add()之后,b = "aaa"就执行了
-- 而我们说,只要没有local,那么创建的变量都是全局的
-- 所以再次打印b就得到了字符串"aaa"
-- 另外如果是在全局的话,即便加上了local,它还是一个全局变量
a = "xx"
local a = "xx"
-- 上面两种方式没有区别,因为这是在全局中定义的,所以即使加上了local也没问题
然后我们来看看如何在lua中定义一个函数,lua中函数的定义规则如下:
function func_name(arg1, arg2) do
statement
statement
statement
...
end
lua函数的参数传递
我们来看看给函数传递参数该怎么做?
function f1(a, b, c)
print(a, b, c)
end
f1(1, 2, 3) -- 1 2 3
f1(1, 2) -- 1 2 nil
f1(1, 2, 3, 4) -- 1 2 3
-- 我们看到如果参数传递的不够,会自动使用nil填充
-- 如果传递多了,会自动丢弃
-- lua中不支持关键字参数传递
-- lua中函数也不支持默认参数,但是通过上面这个特性,我们可以实现默认参数的效果
function f2(a, b, c)
-- 我们希望给c一个默认参数,假设就叫"xxx"吧
-- 如果c为nil,那么结果就是"xxx",因为lua中false和nil为假
c = c or "xxx"
print(a, b, c)
end
f2(1, 2, 3) -- 1 2 3
f2(1, 2) -- 1 2 xxx
lua函数的返回值
lua中支持多返回值
-- 比如我们之前使用的string.find函数,也是返回了两个值
function f1()
-- 使用return返回,如果没有return,那么相当于返回了一个nil
end
x = f1()
print(x) -- nil
function f2()
return 1, 2
end
-- 接收的变量和返回值一一对应
x = f2()
-- 所以x是返回值的第一个值,这一点和python不同,python则是一个元组
print(x) -- 1
x, y, z = f2()
print(x, y, z) -- 1 2 nil
-- 如果接收的变量多于返回值的个数,那么剩下的变量使用nil填充
然后看一下lua中的一些特殊情况
function f1()
return "a", "b"
end
x, y = f1()
print(x, y) -- a b
x = f1()
print(x) -- a
x, y, z = f1()
print(x, y, z) -- a b nil
-- 上面的都很好理解
x, y, z = 10, f1()
print(x, y, z) -- 10 a b
-- 这个也简单
-- 那么,下面的结果如何呢?
x, y, z = 10, f1(), 11
print(x, y, z) -- 10 a 11
-- 我们看到只用了f1返回的一个值
x, y, z = f1(), 10
print(x, y, z) -- a 10 nil
-- 惊了,难道不应该是 a b 10吗
lua的返回值有如下规律:
如果等号的右边只有一个函数调用,比如x, y, z = f1(),那么f1的所有的返回值都会被使用,分别按照顺序分配给x、y、z三个变量,不够的赋为nil,多余的丢弃
如果等号的右边除了函数调用,还有其它的值,比如:x, y, z = f1(), 10 那么如果调用不是在最后一个,那么只返回一个值,如果在最后一个,那么会尽可能的返回多个值
怎么理解呢?
比如:x, y, z = f1(), 10,显然f1()的右边还有值,那么不好意思,不管f1返回的多少个值,只有第一个有效。x, y, z = 10, f1(),f1()的右边没有值了,显然它是最后一个,那么要尽可能的返回多个值,10给了x,那么f1返回的"a"和"b"就会给y和z。
如果是x, y, z = 10, 20, f1(),这个时候10和20会赋值给x和y,那么尽管f1返回两个值,但是只剩下一个变量了,所以f1的第一个返回值会赋值给z
再次举例说明
function f1()
return "a", "b"
end
-- f1()后面没有东西了,位于最后一个,因此尽可能匹配更多的返回值
x, y, z = 10, f1()
print(x, y, z) -- 10 a b
-- f1返回两个值,加上后面的10正好三个,看似能够匹配x y z
-- 但是f1()是一个函数调用,它的后面还有东西,因此在这种情况下,我们说f1只有一个返回值生效
-- 如果f1没有返回值,那么相当于返回了一个nil
-- 所以按照顺序匹配的话,x = f1的第一个返回值,y = 10, z = nil
x, y, z = f1(), 10
print(x, y, z) -- a 10 nil
-- 尽管f1()在最后面,但我们说是尽可能多的匹配
-- x和y已经找到10和20了,所以只能是f1的第一个返回值赋值给z
x, y, z = 10, 20, f1()
print(x, y, z) -- 10 20 a
-- 显然此时已经轮不到f1了
x, y, z = 10, 20, 30, f1()
print(x, y, z) -- 10 20 30
function f2() end
-- 即使f2什么也没有返回,但是会给一个nil
x, y, z = f2(), 10
-- 所以x是nil,y是10,z是nil
print(x, y, z) -- nil 10 nil
相信此时你对lua中函数的返回值已经有一个大致的了解了,但是我们看到上面的例子中,函数调用的右边只是一个普通的值,如果是多个函数调用怎么办?我们来看看
function f1()
return 1, 2
end
function f2()
return 3
end
x, y, z = f1(), f2()
print(x, y, z) -- 1 3 nil
-- 我们看到结果和之前是类似的
-- f1()后面还有东西,尽管不是普通的值,但不管咋样,有东西就对了
-- f1()不是最后一个,那么不好意思,只有返回值的第一个会赋值给变量
-- 因此1会赋值给x,f2()位于最后一个,会尽可能多的匹配,但是只有一个值
-- 因此f2返回的3,会赋值给y,z的话就是nil
x, y, z = f1(), f2(), "xx"
print(x, y, z) -- 1 3 xx
-- 如果f2返回了两个值呢?
function f2() return 3, 4 end
x, y, z = f1(), f2()
print(x, y, z) -- 1 3 4
-- 很好理解
x, y, z = f1(), f2(), "xx"
print(x, y, z) -- 1 3 xx
-- f1调用和f2调用后面都有东西,因此都只有返回的第一个值生效
lua中函数的返回值我们已经揭开它的庐山真面目了,但是函数的返回值还有一个特点, 我们来看一下
function f1()
return "a", "b"
end
print(f1()) -- a b
-- 这没有问题
-- 我们看到函数依旧无法摆脱这个命运
-- 即便是打印,如果后面还有东西,那么只有自身的第一个返回值会被打印出来
print(f1(), 1) -- a 1
-- 对于其它的函数也是如此
print(string.find("hello world", "wor")) -- 7 9
print(string.find("hello world", "wor"), "xxx") -- 7 xxx
-- 事实上不光是print,我们知道函数的返回值可以作为另一个函数的参数
function f1() return 1, 2 end
function f2(a, b, c) print(a, b, c) end
-- 我们看到,除了赋值,作为另一个函数的参数,也是如此
f2(f1()) -- 1 2 nil
f2("xx", f1()) -- xx 1 2
f2(f1(), "xx") -- 1 xx nil
-- 同理,即便是对于表,也是一样的
t = {f1() }
-- 很好理解,元素个数为2,就是f1的返回值
print(#t, t[1], t[2]) -- 2 a b
t = {f1(), "xxx" }
print(#t, t[1], t[2]) -- 2 a xxx
-- 惊了,我们看到明明加进去一个元素,居然还只有两个元素
-- 说明即使在表中,只要函数调用后面有东西,函数的返回值只有第一个生效
-- 最后lua函数还有一个特点
-- 如果将函数调用,再次使用括号括起来,那么强制只有第一个返回值返回
a, b = f1()
print(a, b) -- a b
a, b = (f1())
print(a, b) -- a nil
-- 当我们使用()将函数调用包起来之后
-- 使得无论函数返回了多少个值,其结果只有第一个值有效
-- 因此对于a, b = (f1())来说,a的结果就是f1函数的第一个返回值,b为nil
lua函数的可变长参数
lua中函数可以通过可变长参数来接收任意个数的参数,通过...来实现。这个...就是lua函数可变长参数的定义方式,我们使用的时候直接使用这个...即可。
-- 可变长参数...一定要位于普通的参数之后
function f1(a, b, ...)
-- 刚才我们举的例子,定义变量都是全局变量
-- 但是工作中,函数里面的变量,如果不需要外接使用,那么一定要定义成局部的
-- 当然即便外接需要使用,也可以通过返回值的方式。只不过为了方便,所以就没有加local关键字
-- 我们来看看这个...是什么玩意
print(..., type(...))
end
-- 首先1会传递给a,2会传递给b,剩余的参数会都传递给...
-- 我们看到它是一个number
f1(1, 2, 3, "xxx") -- 3 number
f1(1, 2, "xxx", 3) -- xxx string
-- 我们似乎看到了一个奇特的现象,我们给...明明传递了两个参数
-- 但是从结果上来看,貌似相当于只传递了一个
-- 我们再来举个栗子
function f2(...)
local a, b = ...
print(a, b)
end
f2("a") -- a nil
f2("a", "b") -- a b
f2("a", "b", "c") -- a b
-- 我们看到...确实不止一个参数,具体是几个则取决于我们传递几个
-- 但是直接打印的时候只有第一个生效,查看类型的时候也是查看第一个值的类型
这个...算是比较奇葩的东西,为什么说奇葩呢?因为它没有特定的类型,这个...只能在函数中使用,至于到底是什么类型,则取决于第一个值的类型,但它又不止一个值。
再来看个栗子:
-- 首先我们print(nil)是可以打印出来东西的,结果就是nil
print(nil) -- nil
-- 一个不存在的变量结果也可以nil相等
print( a == nil) -- true
function f1(...)
print(... == nil)
end
-- 我们看到当我们什么也不传递的时候,结果等于nil
f1() -- true
-- 但是如果我们尝试打印这个...的时候,是打印不出来nil的
function f2(...)
print(type(...))
end
f2() -- 代码报错
--[[
C:\lua\lua.exe: lua/5.lua:16: bad argument #1 to \'type\' (value expected)
stack traceback:
[C]: in function \'type\'
lua/5.lua:16: in function \'f2\'
lua/5.lua:19: in main chunk
[C]: in ?
]]
-- 我们看到执行函数f2的时候报错了,提示我们:type函数需要一个value
-- 但我们明明传递了一个...过去啊
-- 如果...在接收不到值的时候,那么它就相当于不存在一样
-- 在和别的值进行比较的时候、或者说赋值的时候,...会是nil。但是在作为函数的参数的时候,则相当于不存在
-- 比如:print(...)你以为会打印出nil吗?答案是不会的,此时的print(...)等价于print()
-- 同理type(...)等价于type(),而type需要一个参数,所以报错了。
所以这个...算是比较奇葩的一个存在,我们可以换个方式来理解,尽管这样肯定是不准确的,但是却可以从某种角度上变得容易理解。
function f1(...)
local a, b = ...
print(a, b, 2 == ..., ...)
end
-- 假设我们执行f1(2, 3, 4)的时候,2,3,4会传递给...
-- 对于赋值来说,你可以认为把...替换成了2,3,4
-- 因此local a, b = ...等价于 local a, b = 2, 3, 4,所以a是2、b是3
-- 但是对于比较、或者作为函数参数来说,可以认为是把...换成了2,3,4中的第一个值
f1(2, 3, 4) -- 2 3 true 2 3 4
-- 2 == ...为什么是true呢?因为...的第一个值是2
-- 如果我们什么也不传递的话,假设是执行f1(),显然没有参数会传递给...
-- 因此此时的...就什么也不是,你就可以认为这个...不存在
-- 如果是赋值或者比较的话,那么...会变成nil,如果不是作为参数的参数等于不存在
-- 因此local a, b = ...等价于local a, b = nil
-- 2 == ...等价于2 == nil, 至于print(...)等于print()
f1() -- nil nil false
-- 我们执行f1()的时候,print只打印了3个值,因为...相当于不存在
-- 当然我们也可以显式的传递nil
function f2(...)
local a, b, c, d = ...
print(a, b, c, d)
end
f2(nil, 1, nil, 2) -- nil 1 nil 2
-- 即便是nil,也会按照顺序原封不动地传递过去
但有些时候,我们不知道...究竟代表了多少个参数,这个时候怎么办呢?答案是变成一个table。
function f1(...)
local t = {... }
print(table.concat(t))
end
-- 此时的t = {...}等价于t = {1, 2, "xxx"}
f1(1, 2, "xxx") -- 12xxx
-- 如果出现了nil,比如:执行f1(1, 2, nil, "xxx")会报错,因为nil不能被合并
function f2(...)
local t = {... }
for idx, val in pairs(t) do
print(idx, val)
end
end
f2(1, 2, nil, "xxx")
--[[
1 1
2 2
4 xxx
]]
-- 我们看到里面的nil并没有被打印出来
-- 因为当中出现了nil,我们说pairs会打印所有值不为nil的
-- 如果是ipairs,那么"xxx"也不会被打印,因为ipairs遍历到nil就结束了
所以遍历...的时候,可以将其放在{}里面变成一个表,但是缺陷是里面会出现nil,尽管在遍历的时候可以使用pairs保留所有部位nil的值,但还是不够完美,我们希望能够将所有的值保留下来。这个时候可以使用table.pack,将...变成一个表,这种做法和{...}的区别就是,前者保留了所有的值,并且还提供了一个额外的值计算表中元素的个数。下面举例说明:
function f1(...)
-- 会返回一个表
-- 这个表和我们平时创建的表虽然都是表,但是调用table.pack返回的表会有一个额外的属性:n
-- 执行t.n会返回表中所有元素的个数,
请发表评论