UH2S1L8——表
注:
笔者学习这节课并记下本篇笔记时,使用的Lua是从github上下载的LuaforWindows,版本为Lua 5.1.5
本篇笔记中记录的所有奇怪的“特性”,都是基于该版本的Lua产生的,笔者将这些学习中遇到的情况忠实的记录了下来
笔者不保证非LuaforWindows的Lua或者更高版本的Lua也会出现这种问题,
如果没遇到本笔记中记录的奇怪特性,应该是Lua版本不一致导致的
表
表(table
)是Lua中非常重要的复杂数据类型,在Lua中所有的复杂类型都是table
诸如数组,字典,甚至面向对象的类都需要通过它来实现
另外记住,Lua的索引从1开始!(反程序员.jpg)
本章代码关键字
1 2 3 4 5 6 7 8 9 10 11
| tableVar = {} #tableVar tableVar[] table.remove() for ... in ipairs() do ... end for ... in pairs() do ... end : table.insert() table.remove() table.sort() table.concat()
|
表的声明
表的声明非常简单,使用大括号声明即可
用表实现数组
想让表作为数组那样声明十分简单,使用大括号包裹一系列的值并赋值即可,值的类型是不限的
(由于在table中使用nil
会导致在取长度上出现相当迷惑的情况,因此不要在table中不要使用****nil
,即使table里可以用****nil
)
从作为数组的表取出某个值也很简单,直接表[索引]
即可,记住,Lua的索引从1开始!(反程序员.jpg)
1 2 3 4 5 6
| print("**********数组************") a = {1, 2, 3, 4, "1231", true, nil} print(a[1]) print(a[5]) print(a[6]) print(a[7])
|
1 2 3 4 5
| **********数组************ 1 1231 true nil
|
数组的长度
和获取字符串长度一样,直接#表
即可,可见#
是通用的获取长度的关键字
但是,要注意,这种方式读取到的长度原理是从1开始数,遇到****nil
就停止计数! 这意味着,如果前面某个元素为****nil
,后面即使还有值也会被忽略掉! (当然,也有例外,详情请看自定义索引部分)
1 2
| a = {1, 2, nil, 4, "1231", true, nil} print(#a)
|
有关#
长度相关的内容,可以参考如下内容:
lua 中求 table 长度 | 菜鸟教程 (runoob.com)
对lua #(取长度)操作符的理解
数组的遍历(长度)
我们可以通过上面得到的长度作为目标值来for循环来遍历作为数组的表
1 2 3 4 5
| print("**********数组的遍历************") a = {1, 2, 3, 4, "1231", true, nil} for i = 1, #a do print(a[i]) end
|
1 2 3 4 5 6 7
| **********数组的遍历************ 1 2 3 4 1231 true
|
但是,#
由于遇nil
则断的特性,这种方法遍历作为数组的表是非常不可靠的
后面我们会使用更好的遍历方法
补充:数组删除元素
由于让表内出现nil
会导致通过#
取到的长度不确定,因此稳妥的删除方法是使用table.remove
方法
第一个参数传入要删除元素的列表,第二个参数传入要删除的元素的索引
1 2 3 4 5 6 7
| a = {1, 2, 3, 4, "1231", true, nil}
table.remove(a, 3)
for i = 1, #a do print(a[i]) end
|
用表实现二维数组
想让表作为二维数组那样声明同样很简单,直接表中嵌套表声明即可
从作为二维数组的表取出某个值也很简单,直接表[索引1][索引2]
即可,记住,Lua的索引从1开始!(反程序员.jpg)
1 2 3 4 5
| print("**********二维数组************") a = {{1, 2, 3}, {4, 5, 6}}
print(a[1][1])
|
1 2
| **********二维数组************ 1
|
二维数组的遍历(长度)
同样可以通过两层for循环使用长度来遍历二维数组
1 2 3 4 5 6 7 8 9 10 11
| print("**********二维数组的遍历************") a = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
for i = 1, #a do b = a[i] for j = 1, #b do print(b[j]) end end
|
1 2 3 4 5 6 7 8 9 10
| **********二维数组的遍历************ 1 2 3 4 5 6 7 8 9
|
当然,和数组的遍历一样,#
由于遇nil
则断的特性,这种方法遍历作为数组的表是非常不可靠的
自定义索引
表的声明里可以自定义某个索引的值为什么,即使这个索引是0或者负数,还是字符串类型,甚至是boolean
值
(当然,nil
就别想了,即使可以这么声明也取不出来,会报错) (自定义索引使用数字索引需谨慎)
声明表时,自定义索引可以在任意的位置,而声明表后各个索引指向的值会跳过自定义索引的值
因此下例的5即使声明时在第5个,它对应的索引仍然是3,因为前面有两个自定义索引的值被跳过了
1 2 3 4 5 6 7
| print("**********自定义索引************") aa = {[0] = 1, 2, 3, [-1] = 4, 5} print(aa[0]) print(aa[-1])
|
1 2 3 4
| **********自定义索引************ 1 4 3
|
注意!由于#
读取表长度的原理是从索引1开始数,遇到****nil
就停止计数!
这意味着跳过#
读取上面的表的长度只能读到3,因为两个自定义索引的值没法计数到
1 2
| aa = {[0] = 1, 2, 3, [-1] = 4, 5} print(#aa)
|
注意!声明使用自定义索引时最好不要使用正数索引!
如果这样做,一旦表声明中非自定义索引的值的数量超出了指定的索引,
则那个自定义索引的值将被覆盖,无论这个自定义值声明在前还是后
1 2 3 4
| aa = {[0] = 1, 2, [2] = 3, [-1] = 4, 5} print(aa[2]) aa = {[0] = 1, 2, 3, [-1] = 4, [2] = 5} print(aa[2])
|
通过这个现象也可以猜测Lua解释器遇到声明表语句时,会先为自定义索引赋值,然后从1开始挨个为索引赋值
自定义索引时最好不要使用正数索引还有另外一个原因,下面就会提及
当声明表时,跳过了一个索引去赋值,使用 #
计算长度时会把跳过的索引也算上
即使你隔一个跳一个,同样会把这些被跳过的索引都计算进去(Lua5.1明确存在该问题,LuaforWindows就是该版本)
1 2 3 4
| aa = {[1] = 1, [2] = 2, [4] = 4, [5] = 5} print(#aa) aa = {[1] = 1, [2] = 2, [4] = 4, [6] = 6} print(#aa)
|
但是,一旦你跳过了两个索引,计数就会中断
1 2
| aa = {[1] = 1, [2] = 2, [5] = 5, [6] = 6} print(#aa)
|
通过长度遍历数组,你会发现被跳过的那一个索引对应的值还是nil
,但也被计数了
(笔者总结的结论就是:自定义索引别用正数!以防迷惑情况的发生)
1 2 3 4 5
| aa = {[1] = 1, [2] = 2, [4] = 4, [6] = 6} print("长度: " .. #aa) for i = 1, 6 do print("aa[i]: " .. tostring(aa[i])) end
|
1 2 3 4 5 6 7
| 长度: 6 aa[i]: 1 aa[i]: 2 aa[i]: nil aa[i]: 4 aa[i]: nil aa[i]: 6
|
迭代器遍历
迭代器遍历主要是用来遍历表的,因为通过#
得到长度是不准确的,因此一般不用#
来遍历表
ipairs遍历
它也是一种for循环,它是从表的索引1开始往后遍历索引与其值,直到遇到nil
停止,
因此这种循环只能遍历表的连续的索引与值,结构如下:
1 2 3
| for 索引变量, 值变量 in ipairs(要遍历的表) do 从索引1开始往后遍历索引与其值,直到遇到索引对应的值为nil就停止 end
|
由于这种循环同样只能从索引1开始遍历连续的索引与值,因此实际使用它遍历表,同样会遇到使用 #
遍历长度的问题
1 2 3 4 5 6
| print("**********ipairs迭代器遍历************") a = {[0] = 1, 2, [-1] = 3, 4, 5, [5] = 6}
for index, value in ipairs(a) do print("ipairs遍历键值: index:" .. index .. " value:" .. value) end
|
1 2 3 4
| **********ipairs迭代器遍历************ ipairs遍历键值: index:1 value:2 ipairs遍历键值: index:2 value:4 ipairs遍历键值: index:3 value:5
|
ipairs遍历可以只遍历索引
1 2 3 4 5 6
| print("**********ipairs迭代器遍历索引************") a = {[0] = 1, 2, [-1] = 3, 4, 5, [5] = 6}
for index in ipairs(a) do print("ipairs遍历键值: index:" .. index .. " value:" .. a[index]) end
|
1 2 3 4
| **********ipairs迭代器遍历索引************ ipairs遍历键值: index:1 value:2 ipairs遍历键值: index:2 value:4 ipairs遍历键值: index:3 value:5
|
pairs遍历
这也是一种for循环,可以遍历表的所有的键(索引)与值,结构如下:
1 2 3
| for 键变量, 值变量 in pairs(要遍历的表) do 遍历表的所有键与值 end
|
可见,它比起使用长度或者ipairs遍历表是更加安全的,它可以将所有的键与值都遍历出来
1 2 3 4 5 6
| print("**********pairs迭代器遍历************") a = {[0] = 1, 2, [-1] = 3, 4, 5, [5] = 6}
for key, value in pairs(a) do print("pairs遍历键值: key:" .. key .. " value:" .. value) end
|
1 2 3 4 5 6 7
| **********pairs迭代器遍历************ pairs遍历键值: key:1 value:2 pairs遍历键值: key:2 value:4 pairs遍历键值: key:3 value:5 pairs遍历键值: key:0 value:1 pairs遍历键值: key:-1 value:3 pairs遍历键值: key:5 value:6
|
pairs遍历同样可以只遍历索引
1 2 3 4 5 6
| print("**********pairs迭代器遍历键************") a = {[0] = 1, 2, [-1] = 3, 4, 5, [5] = 6}
for key in pairs(a) do print("pairs遍历键值: key:" .. key .. " value:" .. a[key]) end
|
1 2 3 4 5 6 7
| **********pairs迭代器遍历键************ pairs遍历键值: key:1 value:2 pairs遍历键值: key:2 value:4 pairs遍历键值: key:3 value:5 pairs遍历键值: key:0 value:1 pairs遍历键值: key:-1 value:3 pairs遍历键值: key:5 value:6
|
用表实现字典
字典是由键值对构成的数据结构,我们可以通过自定义索引来让表像是字典那样声明出来
你也可以像字典那样通过表[键]
来获取值,也可以如面向对象的对象那样通过表.键
来获取值
但是要注意,诸如表.1
这样的表.数字
是会报错的,这意味着用作数组的表不能通过表.索引
获取值
1 2 3 4 5 6
| print("**********字典的声明************") a = {["name"] = "唐老狮", ["age"] = 14, ["1"] = 5} print(a["name"]) print(a["age"]) print(a["1"]) print(a.name)
|
1 2 3 4 5
| **********字典的声明************ 唐老狮 14 5 唐老狮
|
字典修改与添加
用作字典的表可以通过表[键]=值
或者表.键=值
来修改或者添加(如果没有就是添加)对应的键值对
1 2 3 4 5 6 7 8 9 10
| a = {["name"] = "唐老狮", ["age"] = 14, ["1"] = 5}
a["name"] = "TLS" print(a["name"]) a.name = "tls" print(a.name)
a["sex"] = false print(a["sex"]) print(a.sex)
|
字典的删除
用作字典的表事实上没有真正意义的删除,只需要表[键]=nil
或者表.键=nil
即可
其实,即使之前没有为某个键赋值,也可以读取那个键的值,只是值为nil
(用作数组的表删除元素不要用这种方式!)
1 2 3 4 5
| a = {["name"] = "唐老狮", ["age"] = 14, ["1"] = 5, ["sex"] = true}
a["sex"] = nil print(a["sex"]) print(a.sex)
|
字典的遍历
由于字典的键往往不是数字形式且不连续,因此不能用ipairs来遍历,必须使用pairs遍历
1 2 3 4 5 6 7 8 9
| a = {["name"] = "唐老狮", ["age"] = 14, ["1"] = 5}
for key, value in pairs(a) do print(key, value) end
for key in pairs(a) do print(key, a[key]) end
|
1 2 3 4 5 6
| 1 5 age 14 name tls 1 5 age 14 name tls
|
要注意,有些地方会用这种_
写法,来忽略键的赋值,但是它实际上并不能像C#那样真的将键赋值忽略,实际上这个_
一样可以获取键
1 2 3 4 5 6 7 8
| a = {["name"] = "唐老狮", ["age"] = 14, ["1"] = 5} for _, value in pairs(a) do print(value) end
for _, value in pairs(a) do print(_, value) end
|
1 2 3 4 5 6
| 5 唐老狮 14 1 5 name 唐老狮 age 14
|
用表实现类
Lua中没有原生的面向对象,即没有class
这种关键字帮我们声明类
这意味着一旦想要在Lua使用面向对象,将不得不去自己用表去实现面向对象的类
(就如C语言我们需要用结构体和指针实现面向对象那样。。。)
首先需要明确,一个类至少包括成员变量,构造函数,成员函数等内容
成员变量与成员方法
成员变量和成员函数在表里很好声明,
首先在表里,可以直接写变量赋值的语句,即变量 = 值
,我们可以通过这种写法模拟成员变量的声明, 外部调用时通过上面提及的表.变量
来调用成员变量
其次,在Lua中函数也是一种变量类型,这意味着我们可以用方法名 = 函数声明
来声明成员方法,外部使用时通过上面提及的表.方法
来调用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Student = { age = 1, sex = true, Up = function () print("我成长了") end, Learn = function() print("好好学习") end, }
print(Student.age) Student.Up()
|
1 2 3
| **********类与结构体************ 1 我成长了
|
很显然,表只模拟成员变量与成员方法并不面向对象,它用起来就像是C#的静态类(不能实例化,只能类名.成员
调用)那样
由于没有去模拟构造函数,这导致我们没有实例化对象的操作,这样的“类”,只能声明一个然后就操作这声明出来的唯一“类对象”
由表模拟的类,外部还可以去添加成员变量和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Student = { age = 1, sex = true, Up = function () print("我成长了") end, Learn = function() print("好好学习") end, }
Student.name = "唐老狮" Student.Speak = function() print("说话") end function Student.Speak2() print("说话2") end
print(Student.name) Student.Speak() Student.Speak2()
|
类内部访问内部变量
由表模拟的类内部,我们不能直接只写成员变量去调用内部成员变量,在表内部访问表的成员变量和方法时,必须要指出是来自哪个表的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Student = { age = 1, sex = true, Up = function() print(age) print("我成长了") end, Learn = function() print("好好学习") end, }
print(Student.age) Student.Up()
|
想要在由表模拟的类内部调用类本身的属性或者方法,一定要指定是哪个表的
笔者个人认为,这是因为Lua没有面向对象,我们在做的实际是调用表中存放的一个函数而已,而非调用某个对象里的方法,
因此自然也不能让函数直接调用某个表里的内容,哪怕该函数实际上存放在表内
因此一种写法就是直接类名.属性
或类名.方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Student = { age = 1, sex = true, Up = function() print(Student.age) print("我成长了") end, Learn = function() print("好好学习") end, }
print(Student.age) Student.Up()
|
第二种写法就是方法添加一个参数,方法内部使用这个参数调用成员,
外部使用这个方法时将类本身作为参数传入,这样方法就可以调用类本身的成员了(类型检查是没有的,只能祈祷外部真的传入了类本身)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Student = { age = 1, sex = true, Up = function() print(Student.age) print("我成长了") end, Learn = function(t) print(t.sex) print("好好学习") end, }
Student.Learn(Student)
|
语法糖 :
很显然上面的写法无论哪种都很不方便,因此Lua提供了一个语法糖类名:方法()
它也是用来调用方法的,和.
调用方法的区别是,该语法糖会自动把调用该方法的表自身作为第一个参数传入进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Student = { age = 1, sex = true, Up = function() print(Student.age) print("我成长了") end, Learn = function(t) print(t.sex) print("好好学习") end, }
Student.Learn(Student) Student:Learn()
|
该语法糖在声明函数时亦可用,相当于函数默认声明了第一个参数self
,可以在函数内部使用self
参数
这意味着,这里的****self
不和python中****self
或者C#中****this
一样,它单纯就是一个 可以配合 外部调用函数时使用的 :
的参数而已
1 2 3 4 5 6 7
| function Student:Speak2() print(self.name .. "说话") end
function Student.Speak2(self) print(self.name .. "说话") end
|
但是,即使在函数声明时使用了:
,调用它时依然需要使用:
或者传入自己(因此你同样要祈祷外部真的知道这方法需要使用:
)
(因为两种用法是两种语法糖,声明函数时是用来自动声明一个self参数,调用函数是是用来自动传入调用者自己)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Student = { age = 1, sex = true, Up = function() print(Student.age) print("我成长了") end, Learn = function(t) print(t.sex) print("好好学习") end, }
Student.name = "唐老狮"
function Student:Speak2() print(self.name .. "说话2") end
Student:Speak2() Student.Speak2(Student)
|
表的公共操作
表的插入
table.insert
向表的指定位置插入某个值
- 第一个参数:要插入到哪个表内
- 第二个参数:要插入到哪个索引处(可省略,略过则默认插入到最后一位)
- 第三个参数:要插入的值
1 2 3 4 5 6 7 8 9 10
| t1 = { { age = 1, name = "123" }, { age = 2, name = "345" } } t2 = { name = "唐老狮", sex = true }
print(#t1) table.insert(t1, 2, t2) print(#t1) print(t1[2].sex)
|
删除指定元素
移除表中指定位置上的元素,并返回这个被移除的值。若不指定位置,默认移除最后一个索引的内容
- 第一个参数:要删除元素的表
- 第二个参数:要删除元素的索引(若不指定索引,则默认删除最后一个)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| t1 = { { age = 1, name = "123" }, { age = 2, name = "345" } } t2 = { name = "唐老狮", sex = true }
table.insert(t1, 2, t2)
table.remove(t1) print(#t1) print(t1[1].name) print(t1[2].name) print(t1[3])
table.remove(t1, 1) print(t1[1].name)
|
排序
顾名思义,第一个参数为需要排序的表,第二个参数为排序规则函数,需要两个参数,并返回布尔值,降序需要在这里设置(可不填)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| t2 = {5, 2, 7, 9, 5} table.sort(t2) for _, value in pairs(t2) do print(value) end
table.sort(t2, function(a, b) if a > b then return true end end) for _, value in pairs(t2) do print(value) end
|
拼接
提供一个列表,确保所有其元素都是字符串或数字,再提供拼接字符,返回拼接好的字符串
1 2 3
| tb = {"123", "456", "789", "10101"} str = table.concat(tb, ";") print(str)
|