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 --ipair遍历,可从索引1开始遍历表,遇到nil就停止
for ... in pairs() do ... end --pair遍历,可以遍历表所有的键与值
: --语法糖,申明函数时是用来自动申明第一个参数self,调用函数时是将调用函数的表自己作为第一个参数传入到函数内
table.insert() --向表的指定位置插入某个值,不指定位置则默认插入到最后一位
table.remove() --除表中指定位置上的元素,并返回这个被移除的值。若不指定位置,默认移除最后一个索引的内容
table.sort() --在表内从list[1]到list[#list]的元素按指定次序排序。
table.concat() --将元素都是字符串或数字的表,和拼接字符拼接起来,返回拼接好的字符串

表的声明

表的声明非常简单,使用大括号声明即可

1
a = {}

用表实现数组

想让表作为数组那样声明十分简单,使用大括号包裹一系列的值并赋值即可,值的类型是不限的
(由于在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)
1
2        --然而按照我们声明的内容,它应该是7!

有关#​长度相关的内容,可以参考如下内容:

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
3
4
5
1
2
4
1231
true

用表实现二维数组

想让表作为二维数组那样声明同样很简单,直接表中嵌套表声明即可
从作为二维数组的表取出某个值也很简单,直接表[索引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 0 1 2 3 (索引)
-- 4 1 2 3 5 (值)
1
2
3
4
**********自定义索引************
1
4
3

注意!由于#​​读取表长度的原理是从索引1开始数,遇到****​nil就停止计数!
这意味着跳过#​读取上面的表的长度只能读到3,因为两个自定义索引的值没法计数到

1
2
aa = {[0] = 1, 2, 3, [-1] = 4, 5}
print(#aa)
1
3

注意!声明使用自定义索引时最好不要使用正数索引!
如果这样做,一旦表声明中非自定义索引的值的数量超出了指定的索引,
则那个自定义索引的值将被覆盖,无论这个自定义值声明在前还是后

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])
1
2
5        --原本声明时[2] = 3,但被覆盖了
3 --原本声明时[2] = 5,但被覆盖了

通过这个现象也可以猜测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
5
6

但是,一旦你跳过了两个索引,计数就会中断

1
2
aa = {[1] = 1, [2] = 2, [5] = 5, [6] = 6}
print(#aa)
1
2

通过长度遍历数组,你会发现被跳过的那一个索引对应的值还是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)
1
2
3
4
TLS
tls
false
false

字典的删除

用作字典的表事实上没有真正意义的删除,只需要表[键]=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)
1
2
nil
nil

字典的遍历

由于字典的键往往不是数字形式且不连续,因此不能用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
唐老狮
说话
说话2

类内部访问内部变量

由表模拟的类内部,我们不能直接只写成员变量去调用内部成员变量,在表内部访问表的成员变量和方法时,必须要指出是来自哪个表的

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()
1
2
3
1
nil --输出了nil,很显然不符合预期
我成长了

想要在由表模拟的类内部调用类本身的属性或者方法,一定要指定是哪个表的

笔者个人认为,这是因为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
1
1
我成长了

第二种写法就是方法添加一个参数,方法内部使用这个参数调用成员,
外部使用这个方法时将类本身作为参数传入,这样方法就可以调用类本身的成员了(类型检查是没有的,只能祈祷外部真的传入了类本身)

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)
1
2
true
好好学习

语法糖 :

很显然上面的写法无论哪种都很不方便,因此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() --等价于上面的写法
1
2
3
4
true
好好学习
true
好好学习

该语法糖在声明函数时亦可用,相当于函数默认声明了第一个参数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)
1
2
唐老狮说话2
唐老狮说话2

表的公共操作

表的插入

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
2
3
true

删除指定元素

移除表中指定位置上的元素,并返回这个被移除的值。若不指定位置,默认移除最后一个索引的内容

  • 第一个参数:要删除元素的表
  • 第二个参数:要删除元素的索引(若不指定索引,则默认删除最后一个)
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) --删除了{ age = 2, name = "345" }
print(#t1)
print(t1[1].name)
print(t1[2].name)
print(t1[3])

table.remove(t1, 1) --删除了{ age = 1, name = "123" }
print(t1[1].name)
1
2
3
4
123
唐老狮
nil
唐老狮

排序

顾名思义,第一个参数为需要排序的表,第二个参数为排序规则函数,需要两个参数,并返回布尔值,降序需要在这里设置(可不填)

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
4
5
6
7
8
9
10
2
5
5
7
9
9
7
5
5
2

拼接

提供一个列表,确保所有其元素都是字符串或数字,再提供拼接字符,返回拼接好的字符串

  • 第一个参数:需要拼接的表
  • 第二个字符:拼接字符
1
2
3
tb = {"123", "456", "789", "10101"}
str = table.concat(tb, ";")
print(str)
1
123;456;789;10101