lua语言(1):安装、基本结构、函数、输入输出

楔子

我们这次来一起学习一下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语言使用作为字符串的结束一样,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:lualua.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会返回表中所有元素的个数,也包括nil。
    -- 需要注意的是:我们平时手动创建的表没有n这个属性,只有调用table.pack返回的表才有这个属性
    local t = table.pack(...)
    -- 获取表的元素个数,我们之前使用的是#t
    -- 对于调用table.pack返回的表也可以这么做,只是结果未必使我们想要的
    print(t.n, #t)
end

f1(nil, nil, 3, 4, 5)  -- 5   5
f1(1, 2, 3, nil, nil)  -- 5   3
f1(nil, nil, 3, 4, nil)  -- 5   0

-- 我们看到无论什么时候,t.n返回的永远是表中元素的个数
-- 但是#t就不一定了,从上面的结果我们可以看到结果让人有点捉摸不透啊
-- 第三个例子,结果直接是0了,是不是很诡异呢?
-- 所以说不要在创建表的时候在里面写nil,这是原罪
-- 因为写上了nil,你不知道元素到底有多少个
-- 比如f1(nil, nil, 3, 4, 5),显然nil是被算进去了的,但是f1(1, 2, 3, nil, nil)中的nil就没有
-- 至于f1(nil, nil, 3, 4, nil)更可怕,别说nil没有算进去,把原本能算进去的两个元素也给拖下水了

-- 但是有些对技术非常专研的人,可能会探究过nil出现的个数、以及出现的位置,对#t产生的影响
-- 甚至发现了一些规律,并且感觉还满靠谱的。但是千万不要认为这是对的,也没有必要去探究
-- 总之不要在表中写nil,但是对于当前这个例子而言,因为表示通过对...打包得到的
-- 而...是认为手动传递的,数量显然是在我们的掌控范围内,而t.n也能准确返回元素个数
-- 所以在...中传递nil是没有问题的

-- 我们也可以进行遍历
function f2(...)
    local t = table.pack(...)
    for i = 1, t.n do
        print(t[i])
    end
end

f2(nil, 1, 2, nil, 3)
--[[
nil
1
2
nil
3
]]

-- 所有的值都被打印的出来

另一种遍历可变长参数的方式是使用select

print(select(1, "a", "b", "c"))  -- a	b	c
print(select(2, "a", "b", "c"))  -- b	c
print(select(3, "a", "b", "c"))  -- c
print(select(4, "a", "b", "c"))  --
print(select("#", "a", "b", "c"))  -- 3

-- 我们看到select(n, "a", "b", "c", ...)的作用是返回第n个元素以及其后的所有元素
-- 如果n表示数字,而是"#"的话,那么会返回所有元素的个数
function f1(...)
    print(select("#", ...))
end

-- select获取元素也是返回包括nil在内的所有元素的个数
print(f1(nil, nil, 3, 4, nil))  -- 5


-- 比如我们可以算出传入的参数中,值为整型的和
function sum(...)
    local s = 0
    for i = 1, select("#", ...) do
        -- 尽管select(i, ...)可能返回多个值,但是在之前我们已经知道
        -- 如果我们只有一个变量接收的话,那么只会将返回的第一个值赋值给val
        local val = select(i, ...)
        if type(val) == "number" then
            -- 这里我们进行了检测,如果能确保传递的一定是number
            -- 那么也可以直接写成 s = s + select(i, ...)
            -- 因为运算的话,也是只有返回的第一个值会参与运算
            s = s + val
        end
    end
    return s
end

print(sum(1, 2, 3, 4))  -- 10
print(sum(1, 2, 3, "xx", 4))  -- 10
print(sum(1, 2, nil, "xx", 4))  -- 7

我们看到了table.pack,可以将...打包成一个表,同理table.unpack也可以对一张表进行解包。

function f1(a, b)
    print(a, b)
end

t = {1, 2, 3 }
f1(table.unpack(t))  -- 1	2


print(
    string.find("mashiro", "shiro")
)  -- 3	7

print(
    string.find(
        table.unpack({"mashiro", "shiro"})
    )
)  -- 3	7

-- 所以传递参数的时候,f1(a, b) <==> f1(table.unpack({a, b}))
--[[
如果熟悉python的话,那么这个类似于python中的*
def f1(a, b):
    pass
    
f1(1, 2) <==> f1(*[1, 2])
]]

table.unpack还可以只对部分元素进行解包。

function f1(a, b)
    print(a, b)
end

t = {1, 2, 3, 4, 5 }
-- 对于索引为2以及后面的元素进行解包,所以结果是2 3 4 5
-- 因此a和b为2、3
f1(table.unpack(t, 2))  -- 1	2
f1(table.unpack(t, 4))  -- 4	5
f1(table.unpack(t, 5))  -- 5	nil
f1(table.unpack(t, 6))  -- nil	nil

-- 对索引为1到5的元素进行解包
f1(table.unpack(t, 1, 5))  -- 1    2
-- 反着写则不存在
f1(table.unpack(t, 3, 1))  -- nil  nil
-- 起始和结束相等,那么结果只有一个值
f1(table.unpack(t, 4, 4))  -- 4  nil

lua函数中的goto

之所以把goto放在最后面,是因为它不是必须存在的,有的人甚至不建议使用goto,但是在某些时候goto还是很有用的,所以还是要介绍一下它

事实上这个goto,肯定不用我说,大家都知道它是用来跳转的。但是我们就来看看lua中的函数如何使用goto来进行跳转。

需要注意的是:lua中的goto只能在函数中使用,当然其它语言也是如此,并且跳转只能在当前的代码块中进行跳转,一个函数不可能通过goto跳转到另一个函数里面去。

function f1(a)
    -- 定义一个标签,通过::标签名::来进行定义
    -- 之所以设置成这样,也是为了给程序员一个提示作用吧
    ::label1::
    print(11)
end

f1()  -- 11

-- 标签定义之后,代码依旧正常执行
-- 没有goto,你可以认为标签相当于不存在


function f2()

    -- 跳转到label1中
    goto label1

    ::label1::
    print(123)
    ::label2::
    print(456)
end

f2()
--[[
123
456
]]
-- 注意:跳转到一个标签的时候,会执行其后面的所有代码
-- 标签只是一个位置:跳转到这个位置,然后执行该位置后面的所有代码

function f3()
    -- 跳转到label2中
    goto label2
    ::label1::
    	print(123)
    ::label2::
    	print(456)
end
f3()  -- 456

通过标签,我们可以使用continue的功能

function f1()
    for i = 1, 5 do
        if i == 3 then
            goto continue
        end
        print(i)
        -- 直接跳转到结尾即可
        ::continue::
    end
end

f1()
--[[
1
2
4
5
]]

lua中的输入、输出

由于lua语言强调可移植性和嵌入性,所以lua本身并没有提供太多与外部交互的机制,从图形、数据、网络访问等等大多数IO操作,应该有宿主程序来完成。如果不考虑一些外部库,但就lua本身来说,只能操作一些基本的文件。

lua中提供了一个库叫做io,专门用来进行操作文件的。当然print函数也是通过io来实现的,因为print进行打印,本质上是将内容写入到控制台当中,因为控制台你也可以理解为一个文件。

io.write("xxx")
io.write("yyy")
-- xxxyyy

-- print也是调用了io.write,但是我们看到最终输出了xxxyyy
-- 因为print自带了换行,io.write是将内容原本输出

至于读取也很简单,使用io.read()可以从命令行中读取一行

-- 执行完之后会卡主
name = io.read()
-- 输入mashiro,然后回车,那么程序会读取输入,赋值给name,执行下面代码
print(name)  -- mashiro


age = io.read()
-- 从控制台当中读取的都是string类型
print(16, type(age))  -- 16   string

读取和写入文件

io.read和io.write,我们可以用来从控制台输入、输出,如果是文件的话,则是io.open(文件名, 模式)

f = io.open("1.txt", "r")
--[[
关于文件的读取模式,有以下几种
r:只读,文本模式,文件必须存在
w:只写,文本模式,不存在会创建,存在则清空
a:追加,文本模式,不存在会创建,存在则追加
rb:只读,二进制模式,文件必须存在
wb:只写,二进制模式,不存在会创建,存在则清空
ab:追加,二进制模式,不存在会创建,存在则追加

r+:可读可写,文件同样要求必须存在,指针会自动位于文件的开头位置
w+:可读可写,文件不存在会创建,存在则清空
a+:可读可写,文件不存在会创建,存在则追加,指针位于文件的结尾
rb+、wb+、ab+也是同理
]]

print(f)  -- file (00007ffff819fa90)
-- 我们看到读取文件得到的是一个userdata类型
print(type(f))  -- userdata

-- f是一个文件句柄,如果想要读取里面的内容,需要使用f:read,注意:不是f.read
-- 关于:运算符,我们会在后面讨论,目前只需要知道通过:来调用read即可
print(f:read("a"))
--[[
when i was young,
i'd listen to the radio,
waiting for my favourite song
]]

-- 但是我们看到,我们在read函数里面传入了一个字符串"a"
-- 如果不传,那么只会读取一行
-- 传入字符串"a",那么会全部读取进来

-- 一旦文件读取完毕,指针会移到文件的结尾,那么再次读取会返回nil
print(f:read())  -- nil

我们说,io.read里面如果不传值,那么会只读取一行,传入"a"则是全部读取,除此之外还可以传入一个数字,读取指定字节的文本。

f = io.open("1.txt", "r")

-- 读取一行,这里不包括换行符
-- 如果想把换行符也读取进来的话,可以通过f:read("L")
-- 而事实上,f:read()等价于f:read("l"),表示读取一行的同时不读取结尾换行符
print(f:read())  -- when i was young,

-- 再次读取40个字符
print(f:read(40))
--[[
i'd listen to the radio,
waiting for my
]]

-- 关闭文件
f:close()

-- 至于写文件我们就不说了,比较简单
-- 总之记得关闭文件,然后内容会写到文件里面,当然程序结束时也会关闭文件
-- 但如果想在程序运行期间强行将内容刷到文件里面去,可以使用f:flush(),和其它编程语言都是类似的

如果读取的时候文件不存在,怎么办?

f, err_msg, err_code = io.open("不存在的文件.txt", "r")
print(f)  -- nil
print(err_msg)  -- 不存在的文件.txt: No such file or directory
print(err_code)  -- 2

-- 如果文件不存在,会返回三个值
-- 分别是nil、错误信息、以及错误码


-- 关于状态码需要解释一下
-- 文件读取实际上调用的是操作系统的接口,文件不存在操作系统会返回一个错误
-- 对于每个错误类型,都有一个状态码,这个状态码所有编程语言都是一样的,因为它们调用的是操作系统的接口
-- 以Python为例,文件不存在会抛出一个OsError
--[[
try:
    open("xxx")
except OSError as e:
    print(e.errno)  # 2
    print(e)  # [Errno 2] No such file or directory: 'xxx'
    print(e.args)  # (2, 'No such file or directory')
]]
-- 状态码都是一样的


-- 另外,我们还可以使用assert函数
f = assert(io.open("不存在的文件", "r"))
--[[
C:lualua.exe: lua/5.lua:27: 不存在的文件: No such file or directory
stack traceback:
	[C]: in function 'assert'
	lua/5.lua:27: in main chunk
	[C]: in ?
]]
-- 这里就报错了,如果文件不存在,那么错误信息会被展示出来
-- 并且这里是发生了异常,程序在此就终止了
-- 但是我们有时候并不知道文件是否存在,所以希望在文件不存在的时候,程序还能往下走
-- 这个时候就不要使用assert,而是读取之后,检测是否为nil

io库还提供了三个预定义的三个C语言流的句柄:io.stdin、io.stdout、io.stderr

-- 从控制台中读取除了io.read(),还可以使用io.stdin:read()
name = io.stdin:read()
-- 写入控制台,除了io.write(name),还可以使用io.stdout:write(name)
io.stdout:write(name, '
')  -- mashiro
-- 除了写入的话,除了io.stdout还有io.stderr,这两者都会将内容输出到控制台
-- 但是io.stderr表示标准错误文件,io.stdout表示标准输出文件
io.stderr:write(name, '
')  -- mashiro

-- io.stderr:write打印出来的内容会变成红色,表示输出的一个错误信息
-- 另外,尽管我们是先调用的io.stdout:write,后调用的io.stderr:write
-- 但是当打印的时候,你会发现红色的"mashiro"在上面,说明io.stderr:write写入的内容先被打印了出来

-- 因为stdout是行缓冲,stderr不带缓冲区,会直接输出,目的是为了更早的看到错误信息

io还提供了input和output,io.read实际上就是io.input():read的缩写、io.write是io.output():write的缩写。

name = io.input():read()
io.output():write(name)  -- mashiro

其他文件操作

文件的指针是可以移动的

f = io.open("1.txt", "r")

-- 我们读取了15个字节,所以指针会偏移15个字节
f:read(15)

-- 调用f:seek()即可获取指针当前的位置
print(f:seek())  -- 15

-- 当然我们还可以移动指针
-- 移动指针有三种方式:
-- f:seek("set", count) 从文件开头的位置向后移动count个字节, 返回移动文件指针的位置
-- f:seek("cur", count) 从指针当前的位置向后移动count个字节, 返回移动文件指针的位置
-- f:seek("end", count) 从文件结尾的位置向后移动count个字节, 返回移动文件指针的位置
-- 如果count小于0,那么就是向前移动 abs(count) 个字节

-- 从开头向后移动11个字节,返回11,当然也可以单独使用f:seek()进行获取
print(f:seek("set", 11))  -- 11
-- 从当前(11)位置向后移动11个字节,结果是11 + 11
print(f:seek("cur", 11))  -- 22
-- 从结尾位置向前移动11个字节,事实上即便指针超出文件字节数量范围也没有错,只不过读取到的是一个nil
print(f:seek("end", -11))  -- 63

-- 如果不传递第二个参数、也就是偏移量的话,那么相当于传递了个0
print(f:seek("set", 0))  -- 0
print(f:seek("cur", 0))  -- 0
-- 事实上,我们可以通过这种办法来计算文件的字节数
-- 直接将指针移动到文件结尾,返回的结果就是文件的字节数
print(f:seek("end", 0))  -- 74

io还提供一个io.tmpfile 会返回一个操作临时文件的句柄,该句柄是以读写模式打开的 当程序 运行结束后,该临时文件会被自动移除(删除)

f = io.tmpfile()

-- 由于是可读写,那么写完之后文件指针会处于结尾
f:write("xxxx")
-- 此时如果想读到内容,需要将文件指针移到开头
f:seek("set", 0)
print(f:read("a"))  -- xxxx

此外,除了io,还有一个os,os.rename可以对文件重命名,os.remove可以删除文件

上述所有函数在报错时,都会返回 nil、错误信息、错误码

其他系统调用

函数os.exit可以用来终止程序的执行,里面可以传递两个参数,都是可选的。第一个参数为程序返回的状态,可以是一个数值0或者布尔true,表示执行成功;第二个参数如果为true,那么会关闭lua状态并调用所有析构器释放所占用的内存(不过这种方式通常是非必要的,因为操作系统会在进程退出时释放其占用的资源)

-- os.getenv可以用来获取环境变量
print(os.getenv("USERNAME"))  -- satori

-- os.execute则是执行系统命令,类似于python中的os.system
-- 其有三个返回值,第一个返回值为布尔型,为true表示成功运行完成
-- 第二个返回值是一个字符串,如果字符串是"exit",表示正常结束,为"signal"表示因信号而中断
-- 第三个返回值是返回状态(前提是正常终结)或者终结该程序的信号代码

-- 下面举例说明
a, b, c = os.execute("cd ..")
print(a)  -- true
print(b)  -- exit
print(c)  -- exit

执行操作系统命令,还有一个io.popen。对于os.execute来说,执行的命令结果会执行显示在控制台,如果你Windows是gbk编码还会产生乱码。但是io.popen则是将内容输入到指定位置

f = io.popen("python 2.py", "r")

print(f:read())  -- C:Userssatori
原文地址:https://www.cnblogs.com/traditional/p/13071336.html