Lua:06---table表类型:表索引、表构造器、数组、序列、table标准库

一、表类型介绍

  • 表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构
  • 使用表,Lua语言可以以一种简单、统一且高效的方式表示数组、集合、记录和其他许多数据结构。也可以使用表来表示包(package)和其他对象(例如当调用math.sin时,对于Lua来说,实际上是以“字符串sin”为键检索“表math”)
  • Lua语言的表本质上是一种辅助数组,这种数组不仅可以使用数值作为索引,也可以使用字符串或者其他任意类型的值作为索引(nil除外)

基本使用

  • 可以使用构造器表达式来创建表。例如
-- 创建一个表a
a = {}

-- 返回表的首地址
a

  • 可以向表中添加元素,并通过索引来获取值。例如:
k = "x"

a[k] = 10       -- 键为"x", 值为10
a[20] = "great" -- 键为20, 值为"great"

a["x"]   -- 获取键"x"的值
a[k]     -- 同上
a[20]    -- 获取键20的值

k = 20   
a[k]     -- 获取键20的值

a["x"] = a["x"] + 1 --将键"x"的值加1
a["x"]

  • 可以有多个表名引用于同一个表,当最后一个引用释放时,垃圾收集器会最终删除这个表。例如:
-- 通过a引用于一个表
a = {}
a["x"] = 10

-- b引用与a,指向于同一个表
b = a
b["x"]

-- b改变表, a也改变
b["x"] = 20
a["x"]

-- 释放a的引用,此时表还在
a = nil
b

-- 释放a的引用,现在没有引用引用于这个表,表释放
b = nil

  • 更多详细内容请参阅下面的文章

二、表索引

  • 同一个表存储的值可以具有不同的类型索引,并且可以按需求增长以容纳新的元素
  • 未初始化的表元素的值为nil,将nil赋值给表元素可以将其删除
  • 例如:
-- 空的表
a = {}

-- 创建1000个新元素
for i = 1, 100 do a[i] = i*2 end

-- 得到索引9的值
a[9]

-- 设置"x"索引的值为10
a["x"] = 10
a["x"]

-- 表中没有名为"y"的索引, 返回nil
a["y"]

把表当做结构体使用

  • 把表当做结构体使用时,可以把表名当做结构体名,然后使用"."调用表的元素,类似于C语言的结构体用法
  • 例如:
-- 创建一个空的表
a = {}

-- 等价于a["name"] = "dongshao"
a.name = "dongshao"
-- 等价于a["age"] = 17
a.age = 17

-- 调用两个结构体成员
a.name
a.age

-- 没有这个成员, 索引返回nil
a.hourse

  • 这种用法与普通用法如何选择使用:
    • a["age"]、a.age这两种的调用都是等价的,但是如何选择使用呢?
    • 通常根据自己的需求而定:
      • 如果你的表中的元素通常是固定的、预先定义好的,那么就使用结构体这种调用方法,例如a.age
      • 如果你的表可以使用任意值作为键,并且可以表中的元素会动态变化,那么就使用平常的语法,例如a["age"]

索引类型的注意事项

  • 索引也是有类型的。例如0和"0"不是同一样,前者是数字类型,后者是字符串类型。所以下面的调用是不等价的:
a = {}

-- 这是两个不同的索引键
a[0] = 0
a["0"] = "0"
  • 当然,如果你也可以进行显示类型的转换来调用。例如:
a = {}

-- 定义两个不同的索引键
a[0] = 0
a["0"] = "0"

-- 这两个索引对应的值是不一样的
type(a[0])
type(a["0"])

-- 获取索引值为0的值
a[0]
-- 同上
a[tonumber("0")]

-- 获取索引值为"0"的值
a["0"]
-- 同上
a[tostring(0)]

  • 对于整型和浮点型的表索引不会出现上面的问题。如果整型和浮点型之间能够进行转换,那么两者指向的是同一个索引。例如
    • 下面通过a[2.0]调用表时,2.0会被转换为2,因此调用的是同一者
a = {}

a[2] = 666
a[2]
a[2.0]

a[2.0] = 777
a[2]
a[2.0]

三、表构造器

  • 表构造器是用来创建和初始化表的表达式

空构造器

  • 最简单的构造器是空{}
  • 这种构造器初始化的表内容是空的
a = {}

列表式构造器

  • 类似于C语言的数组,可以指定元素,其中每个元素对应一个默认的索引
  • 默认的索引从1开始(而不是0),并且依次类推
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thuray", "Friday", "Saturday"}

-- 每个元素都有默认的索引, 从1开始
days[1]
days[2]
days[3]
days[4]
days[5]
days[6]
days[7]

纪录式构造器

  • 纪录式构造器可以在定义的时候同时指定索引和对应的值
a = {x = 10, y =20, z = "HelloWorld"}

-- 这样是正确的调用
a["x"]
-- 这样是错误的, 当前作用域没有名为x的变量
a[x]

-- 同上
a["y"]
a[y]

a["z"]

  • 定义这种表时,表的索引有约束:
    • 索引的名字必须与Lua变量的命名规则一样,也就是说必须由下划线、字母、数字组成(不能以数字开头)
    • 并且定义的时候不能加双引号或者单引号
    • 例如:
-- 正确的
a = {x1 = 10}
-- 错误的, x1不可以带双引号
a = {"x1" = 10}

-- 下面的都是错误的, 索引的名字没有符合变量的命名规则
b = {1b = 20}
b = {2c = 20}

"纪录式"与"列表式"混合使用

  • 可以将纪录式和列表式的构造器进行混合使用
  • 当混合使用时:
    • 对于列表式的元素,元素默认索引为1,然后依次类推
    • 纪录式元素的索引以其定义的为准
  • 例如:
    • 下面为纪录式和列表式的混合定义
    • coloe、thickness、npoints、value等都是纪录式,定义时直接给出了索引和值
    • 其他的都是记录时的,没有给出索引,由表自动给出,索引为1的为{x = 0, y = 0},其也是一个表类型...以此类推
polyline = {
    color = "blue", thickness = 2, npoints = 4,
    {x = 0, y = 0},  -- polyline[1]
    {x = 10, y = 0}, -- polyline[2]
    666,             -- polyline[3]
    value = 5, 
    "HelloWorld"     -- polyline[4]
}


print(polyline["color"]) -- blue
print(polyline["value"]) -- 5

print(polyline[1].x) -- 0
print(polyline[2].x) -- 10
print(polyline[3])   -- 666
print(polyline[4])   -- HelloWorld
  • 不论是哪种方式创建表,都可以随时增加或删除表元素。例如
-- 创建一个表w
w = {x = 0, y = 0, label = "console"}
-- 创建一个表x
x = {math.sin(0), math.sin(1), math.sin(2)}

-- 把键1加入到表w中
w[1] = "another field"
-- 把键"f"加入到表x中, 并且键f对应为的元素为表w
x.f = w

w["x"]
w[1]

x[1]      -- 调用索引1处的值, 即为math.sin(0)
x.f[1]    -- 因为f索引对应的是一个表, 因此我们这就是调用其对应的表中键为1的元素
x["f"][1] -- 同上

-- 删除w的x键
w.x = nil

索引局限性和解决方法(方括号索引)

  • 列表式构造器和纪录式构造器有一些局限性,例如:
    • 不能使用负数索引初始化元素(索引必须是以1作为开始)
    • 纪录式构造器中不能使用不符合规范的标识符作为索引(规定索引的名字必须与变量命名规则一样,见上)
    • 纪录式构造器中的索引名不需要使用双引号或者单引号
  • 例如,下面是一些错误用例与改正演示:
-- 错误的, +和-不符合索引命名规则
opnames = { + = "add", - = "sub" }

-- 错误的,索引虽然正确,但是不需要使用双引号
opnames = { "a" = "add", "b" = "sub" }

  • 对于这些局限性,可以使用方括号括起来的表达式显式地指定每一个索引。例如:
-- 正确的,使用方括号指明索引
opnames = { ["+"] = "add", ["-"] = "sub"}

-- 调用
opnames["+"]
s = "-"
opnames[s]

-- 可以使用数字作为索引, 并且索引值还可以为负的
i = 1
a = {[-1] = "A", [0] = "A".."A", [i] = "A".."A".."A"}

a[-1]
a[0]
a[1]

  • 可以看出这种方括号形式的索引带来了很多扩展型。例如,下面的几种表达式就相互等价
{x = 0, y = 0}   -- 等价于{["x"] = 0, ["y"] = 0}

{"r", "g", "b"}  -- 等价于{[1] = "r", [2] = "g", [3] = "b"}

构造器的相关注意事项

  • 构造器的最后一个元素后面可以跟着一个逗号,但是是可选的,可以不添加
-- 尾部逗号默认省略
a = {[1] = "read", [2] = "green"}

-- 同上, 尾部有一个逗号
a = {[1] = "read", [2] = "green",}

  • 构造器中的逗号可以用分号代替,这主要是为了兼容Lua语言的旧版本,目前不会被用到。例如:
a = {[1] = "red", [2] = "green"}
a[1]
a[2]

-- 逗号换成分号也正确, 主要是为了兼容旧版本
a = {[1] = "red"; [2] = "green"}

a[1]
a[2]

四、数组、序列

  • 如果向表示常见的数组(array)或列表(list),只需要使用整型作为索引的表即可

数组

  • 例如下面声明一个表,然后向里面添加元素,元素的索引都是整型,这有点类似于C语言等的数组
a = {}

-- 先输入5个元素
for i = 1, 5 do
    a[i] = io.read()
end

-- 打印5个元素
for i = 1, 10 do
    print(a[i])
end

  • 备注:在Lua语言中,数组索引一般从1开始(而不是像C语言一样从0开始),Lua中的其它许多机制也遵循这个管理
  • 数组的长度:
    • 当操作列表数组时,往往需要知道列表的长度
    • 我们可以将列表的长度存放在常量中,或者存放在其它变量或数据结构中,当然也可以把列表的长度保存在表的某个非数值类型的字段中(由于历史原因,这个键常用是"n"),当然列表的长度经常也是隐藏的
  • 因为未初始化的元素均为nil,所以可以利用nil来标记列表的结束

序列

  • 当一个列表拥有一定数量的元素时,并且列表中不存在空洞时(即所有元素均不为nil,所有索引的值都是连续的),那么我们把这种所有元素都不为nil的数组称为序列
  • 例如:
-- 是数组, 但不是序列
a = {[1] = "a", [3] = "c"}

-- 是数组, 并且是序列
a = {[1] = "a", [2] = "b", [3] = "c"}

五、列表长度、序列长度

  • 可以通过操作符#来获取序列的长度。例如:
-- 定义一个序列, 获取序列的长度
a = {[1] = "a", [2] = "b", [3] = "c", [4] = "d"}
#a

-- 可以使用序列长度对序列进行操作
a[#a]           -- 获取a序列的最后一个元素
a[#a] = nil     -- 移除a序列的最后一个元素
a[#a + 1] = "d" -- 把d添加到序列的尾部

  • 注意,#操作符只有对序列才有效,如果一个表元素中间存在空洞(nil值)时,那么长度操作符是不可考的
  • 将长度操作符#用于存在空洞的列表的行为是Lua语言中最具争议的内容之一:
    • 再过去几年中,很多人建议在操作存在空洞的列表时直接抛出异常;也有人建议扩展长度操作符的语义
    • 然而,这些建议都是说起来容器但是做起来难,其根源在于列表实际上是一个表,而对于表来说,“长度”的概念在一定程度上是不容易理解的
  • 另一个常见的建议是让#操作符返回表中全部元素的数量虽然这种语义听起来清晰且定义明确,但并非特别有用和符合直觉
  • 更复杂的是列表以nil结尾的情况。请问列表的长度是多少:
a = {10, 20, 30, nil, nil}
  • 请注意,对于Lua来说,一个为nil的字段和一个不存在的元素没有区别。因此,上述列表与{10, 20, 30}是等价的(长度为3,而不是5)
  • 可以将以nil结尾的列表当做一种特殊情况。不过,很多列表通过逐个添加各个元素创建出来的。任何按照这种方式构造出来的带有空洞的列表,其最后一个存在为nil的值
  • 总结(重点):
    • 讨论了这么多,程序中的大多数列表其实都是序列(例如不能为nil的文件行)。正因如此,在大多数情况下使用长度操作符是安全的
    • 在确实需要处理存在空洞的列表时,应该将列表的长度显式地保存起来(重点)

六、遍历表

pairs迭代器遍历

  • 不论列表是否为序列,都可以使用这种遍历方式。例如:
-- 不是序列
t = {10, print, x = 12, k = "hi"}
for k, v in pairs(t) do
    print(k, v)
end

-- 是个序列, 因为所有元素的索引都有顺序
t = {10, print, 12, "hi"}
for k, v in pairs(t) do
    print(k, v)
end

  • 受限于Lua语言中的底层实现机制遍历过程中元素的出现顺序是随机的

ipairs迭代器遍历

  • 如果列表是序列,那么可以使用这种方法进行输出;如果表不是序列,那么不可以使用这种输出
  • 并且这种输出的顺序是按照表定义时的顺序输出的
  • 例如:
-- 不是序列, 只会输出前面连续的内容, 后面的内容不会输出
t = {10, print, x = 12, k = "hi"}
for k, v in ipairs(t) do
    print(k, v)
end

-- 是序列, 按照顺序输出所有元素
t = {10, print, 12, "hi"}
for k, v in ipairs(t) do
    print(k, v)
end

数值型for循环

  • 当表为数组类型时,可以通过数值型for循环来输出表中的每一个元素
  • 这种循环一般用于序列式的表,因为其会根据索引值和表长度来输出元素
  • 例如:
t = {10, print, 12, "hi"}

for k = 1, #t do
    print(k, t[k])
end

七、安全访问

  • 考虑下面的场景:我们想确认在指定的库中是否存在某个函数,如果我们确定这个库确实存在,那么可以直接使用if  lib.foo  then...;否则就得使用形如if  lib  and  lib.foo then...的表达式
  • 当表的嵌套深度变得比较深时,这种写法就会很容易出错,例如:
zip = company and company.director and
        company.director.address and
        company.director.address.zipcode
  • 这种写法不仅冗长而且低效,该写法在一次成功的访问中对表进行了6次访问而非3次访问
  • 对于这种情景,诸如C#的一些编程语言提供了一种安全访问操作符。在C#中,这种安全访问操作符被标记为"?."。例如,对于表达式a ?.b,当a为nil时,其结果是nil而不会产生异常。使用这种操作符,可以将上例改写为:
zip = company ?. director ?. address ?. zipcode
  • 如果上述的成员访问过程中出现nil,安全访问操作符会正确的处理nil并最终返回nil
  • Lua语言并没有提供安全访问操作符,并且认为也不应该提供这种操作符:
    • 一方面,Lua语言在设计上力求建立
    • 另一方面,这种操作符也是非常有争议的,很多人就无理由地认为该操作符容器导致无意的编程错误
  • 我们可以使用其他语句在Lua语言中模拟安全访问操作符:
    • 可以使用Lua的or逻辑运算符,对于a or {},如果a为nil,那么其就会返回{}(空表)
    • 因此,对于表达式(a or {}).b的意思为:当a为nil时其结果也同样是nil
  • 这样,我们可以把之前的例子改写为:

zip = (((company or {}).director or {}).address or {}).zipcode
  • 再进一步,可以写的更短和更高效:
-- 可以在其他类似表达式中复用
E = {}

zip = (((company or E).director or E).address or E).zipcode
  • 确实,上面的语法比安全访问操作符更加复杂。不过尽管如此,表中的每一个字段名都只被使用了一次,从而保证了尽可能少地对表进行访问;同时,还避免了向语言中引入新的操作符

八、表标准库

  • 表标准库提供了操作列表和序列的一些常用函数(也可以认为是“列表库”,或者“序列库”,这两个是之前的概念)

table.insert()

  • 该函数的一种形式为:向序列的指定位置插入一个元素,其他元素依次后移。例如:
-- 创建一个表, 并打印所有元素
t = {10, 20, 30}
for k = 1, #t do
    print(k, t[k])
end

-- 在索引1处插入1个元素
table.insert(t, 1, 15)
for k = 1, #t do
    print(k, t[k])
end

  • 该函数的另一种形式为:插入时不指定位置,那么会把新元素插入到序列的尾部。例如:
-- 从标输入中按行读入内容并保存到一个序列中
t = {}
for line in io.lines() do
    table.insert(t, line)
end

print(#t)

table.remove()

  • 该函数的一种形式为:该函数删除并返回序列指定位置的元素,然后将后面的元素向前移动填充。例如:
t = {111, 222, 333}
for k = 1, #t do
    print(k, t[k])
end

-- 删除第2个元素
table.remove(t, 2)
for k = 1, #t do
    print(k, t[k])
end

  • 该函数的另一种形式为:删除时不指定位置,那么会把末尾的元素进行删除。例如:
t = {111, 222, 333}
for k = 1, #t do
    print(k, t[k])
end

-- 删除末尾的元素
table.remove(t)
for k = 1, #t do
    print(k, t[k])
end

利用table.insert()和table.remove()实现堆、队列、双端队列

  • 借助这两个函数,可以很容易地实现栈、队列和双端队列
  • 以栈为例:
-- 创建一个空栈
t = {}

-- 入栈, 在尾部插入元素(push)
table.insert(t, x)

-- 出栈, 删除尾部的元素(pop)
table.remove(t)

table.move()

  • Lua 5.3引入了这个函数
  • 该函数的一种用法为:table.move(a, f, e, t)。调用该函数可以将表a中从索引f到e的元素(包括f和e本身)拷贝到位置t上
  • 例如:下面在列表a的开头插入一个元素
a = {1, 2}

-- 将所有元素依次向后移动1位,表会边打
table.move(a, 1, #a, 2)
for k = 1, #a do
    print(k, a[k])
end

-- 添加一个元素到头部
a[1] = 10
for k = 1, #a do
    print(k, a[k])
end

  • 例如:下面删除列表a的开头元素
a = {1, 2}

-- 将所有元素依次向前移动1位(除了最后一个)
table.move(a, 2, #a, 1)
for k = 1, #a do
    print(k, a[k])
end

-- 将最后一个元素置为nil
a[#a] = nil
for k = 1, #a do
    print(k, a[k])
end

  • 移动实际上是将一个值从一个地方拷贝到另一个地方。因此上面的删除操作最后必须显式地把最后一个元素删除
  • 该函数的另一种用法为:table.move(a, f, e, t, b)。意思为:把表a从索引f到e的元素(包括f和e本身)移动到表b位置t上,函数返回表b的位置
  • 例如:下面把表a的元素移动到另一个空表中,并返回该空表的地址
a = {1, 2}

table.move(a, 1, #a, 1, {})
  • 例如:下面把表a的所有元素拷贝到表b的尾部
a = {1, 2}
b = {3, 4}

table.move(a, 1, #a, #b + 1, b)
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值