6.4 表的应用场景 Lua 表 (Table) 的四种应用场景深度解析 Lua 的表 (Table) 是其最核心、最强大的数据结构。它不仅仅是简单的哈希表或数组,而是一种高度灵活、多用途的工具,几乎在 Lua 编程的方方面面都扮演着至关重要的角色。 掌握表的应用场景,对于深入理解 Lua 语言和编写高效、优雅的 Lua 代码至关重要。 作为数组 (Arrays/Lists) 使用: 表的数字索引特性使其天然适合表示有序的数据集合,即数组或列表。我们将学习如何创建、访问、遍历和操作表作为数组使用。 作为字典 (Dictionaries/Maps) 使用: 表的键值对 (key-value) 结构使其成为理想的字典或映射表。我们将学习如何使用非数字键来存储和检索数据,以及字典的常见操作。
Lua 的表 (Table) 是其最核心、最强大的数据结构。它不仅仅是简单的哈希表或数组,而是一种高度灵活、多用途的工具,几乎在 Lua 编程的方方面面都扮演着至关重要的角色。 掌握表的应用场景,对于深入理解 Lua 语言和编写高效、优雅的 Lua 代码至关重要。
作为数组 (Arrays/Lists) 使用: 表的数字索引特性使其天然适合表示有序的数据集合,即数组或列表。我们将学习如何创建、访问、遍历和操作表作为数组使用。
作为字典 (Dictionaries/Maps) 使用: 表的键值对 (key-value) 结构使其成为理想的字典或映射表。我们将学习如何使用非数字键来存储和检索数据,以及字典的常见操作。
作为记录/结构体 (Records/Structures) 使用: 表可以模拟面向对象编程中的记录或结构体,将相关的数据组织在一起。我们将学习如何使用表来表示复杂的数据实体,并进行数据访问和操作。
作为集合 (Sets) 使用: 表的键的唯一性可以用来实现集合数据结构,用于存储不重复的元素并进行集合运算。我们将学习如何使用表来表示集合,并实现常见的集合操作如添加、删除、判断存在和集合运算。
1. 作为数组 (Arrays/Lists) 使用
在许多编程语言中,数组是一种基本的数据结构,用于存储一系列有序的元素。Lua 的表可以通过使用数字索引来实现数组的功能。Lua 中的数组索引默认从 1 开始,这与许多其他语言(如 C, Python, Java)从 0 开始不同,需要特别注意。
代码实践 1.1: 创建和初始化数组
-- 创建一个空数组 local myArray = {} -- 创建并初始化数组,直接赋值 local myArray1 = {10, 20, 30, 40, 50} -- 创建混合类型的数组 local myArray2 = {1, "hello", 3.14, true} -- 打印数组内容 print("myArray1:", myArray1[1], myArray1[2], myArray1[3], myArray1[4], myArray1[5]) print("myArray2:", myArray2[1], myArray2[2], myArray2[3], myArray2[4])
内容详解 1.1:
local myArray = {}: 这是创建空表的最基本方式。{} 表示创建一个新的空表,并将其赋值给变量 myArray。 此时 myArray 可以作为数组使用。
local myArray1 = {10, 20, 30, 40, 50}: 这种方式在创建表的同时直接初始化了数组元素。 花括号 {} 内的元素按照顺序排列,Lua 会自动为它们分配从 1 开始的数字索引。 myArray1[1] 将访问到 10,myArray1[2] 访问到 20,以此类推。
local myArray2 = {1, "hello", 3.14, true}: Lua 表的强大之处在于它可以存储不同类型的数据在同一个表中。 myArray2 包含了数字、字符串、浮点数和布尔值。
print("myArray1:", myArray1[1], myArray1[2], ...): 使用 myArray1[索引] 的方式访问数组元素。 索引必须是数字,且有效索引范围从 1 到数组的长度。
代码实践 1.2: 获取数组长度
local myArray = {10, 20, 30, 40, 50} -- 使用 # 运算符获取数组长度 local arrayLength = #myArray print("数组 myArray 的长度:", arrayLength) -- 输出: 数组 myArray 的长度: 5 -- 注意:# 运算符对于稀疏数组可能不准确 local sparseArray = {} sparseArray[1] = 1 sparseArray[5] = 5 sparseArray[10] = 10 print("稀疏数组 sparseArray 的长度:", #sparseArray) -- 输出结果可能不确定,取决于 Lua 版本和实现细节
内容详解 1.2:
#myArray: # 运算符是 Lua 中获取表长度的常用方法。 当表被用作数组时,# 运算符通常返回数组中元素的个数,即最大的数字索引值。
稀疏数组的注意点: 当数组中存在 “空洞”(即某些数字索引没有赋值),# 运算符的行为在稀疏数组中可能变得不确定。 对于稀疏数组,# 运算符可能不会准确返回预期的长度。 如果需要处理稀疏数组,建议使用其他方法来追踪有效元素的数量,例如手动计数或使用 table.maxn (Lua 5.1 及更早版本,在 Lua 5.2+ 中已弃用,不推荐使用)。
代码实践 1.3: 遍历数组
local myArray = {"apple", "banana", "cherry", "date"} -- 使用 for 循环和数字索引遍历 print("使用 for 循环和数字索引遍历:") for i = 1, #myArray do print("索引:", i, "值:", myArray[i]) end -- 使用 ipairs 迭代器遍历 print("\n使用 ipairs 迭代器遍历:") for index, value in ipairs(myArray) do print("索引:", index, "值:", value) end
内容详解 1.3:
for i = 1, #myArray do ... end: 这是使用标准 for 循环结合数字索引遍历数组的常见方式。 循环从索引 1 开始,到数组长度 #myArray 结束,每次迭代访问 myArray[i] 获取元素值。
for index, value in ipairs(myArray) do ... end: ipairs 是 Lua 提供的一个迭代器,专门用于遍历数组类型的表。 它会按照数字索引的顺序迭代表中的元素,并返回索引和对应的值。 ipairs 保证了按照数字索引的顺序遍历,并且在遇到非数字索引或空洞时会停止迭代。 因此,ipairs 更适合用于遍历数组,特别是当数组可能存在非数字键或稀疏情况时,它比简单的 for 循环更安全可靠。
代码实践 1.4: 添加和删除数组元素
local myArray = {"apple", "banana", "cherry"} -- 在数组末尾添加元素 table.insert(myArray, "date") print("添加 'date' 后:", table.concat(myArray, ", ")) -- 输出: 添加 'date' 后: apple, banana, cherry, date -- 在指定位置插入元素 table.insert(myArray, 2, "apricot") -- 在索引 2 的位置插入 "apricot" print("在索引 2 插入 'apricot' 后:", table.concat(myArray, ", ")) -- 输出: 在索引 2 插入 'apricot' 后: apple, apricot, banana, cherry, date -- 删除数组末尾元素 table.remove(myArray) print("删除末尾元素后:", table.concat(myArray, ", ")) -- 输出: 删除末尾元素后: apple, apricot, banana, cherry -- 删除指定位置的元素 table.remove(myArray, 2) -- 删除索引 2 的元素 "apricot" print("删除索引 2 的元素后:", table.concat(myArray, ", ")) -- 输出: 删除索引 2 的元素后: apple, banana, cherry
内容详解 1.4:
table.insert(myArray, value): table.insert 函数用于向数组中插入元素。 当只提供两个参数时,它默认将 value 添加到数组的末尾,并将后续元素的索引向后移动一位。
table.insert(myArray, pos, value): 当提供三个参数时,table.insert 将 value 插入到数组的索引 pos 位置,并将索引 pos 及之后的所有元素索引向后移动一位。
table.remove(myArray): table.remove 函数用于从数组中删除元素。 当只提供一个参数时,它默认删除数组的最后一个元素,并返回被删除的元素。
table.remove(myArray, pos): 当提供两个参数时,table.remove 删除数组索引 pos 位置的元素,并将索引 pos 之后的所有元素索引向前移动一位,并返回被删除的元素。
table.concat(myArray, ", "): table.concat 函数用于将数组中的所有元素连接成一个字符串,第二个参数 , 是元素之间的分隔符。 这里用于方便地打印数组内容。
2. 作为字典 (Dictionaries/Maps) 使用
除了数字索引,Lua 表还可以使用任何非 nil 值(除了 NaN,Not a Number)作为键,包括字符串、表、函数等。 这使得表非常适合用作字典或映射表,用于存储和检索键值对数据。
代码实践 2.1: 创建和初始化字典
-- 创建空字典 local myDict = {} -- 创建并初始化字典,使用字符串作为键 local myDict1 = { name = "Alice", age = 30, city = "New York" } -- 创建字典,使用字符串和数字混合作为键 local myDict2 = { ["name"] = "Bob", [25] = "age", occupation = "Engineer" -- 等价于 ["occupation"] = "Engineer" } -- 打印字典内容 print("myDict1 name:", myDict1.name, "age:", myDict1.age, "city:", myDict1.city) print("myDict2 name:", myDict2["name"], "age key:", myDict2[25], "occupation:", myDict2.occupation)
内容详解 2.1:
local myDict = {}: 同样使用 {} 创建空表,可以作为字典使用。
local myDict1 = { name = "Alice", age = 30, city = "New York" }: 这是使用字符串作为键来初始化字典的常见方式。 name = "Alice" 相当于 ["name"] = "Alice" 的语法糖,更加简洁易读。 键 name, age, city 都是字符串,值分别是 "Alice", 30, "New York"。
local myDict2 = { ["name"] = "Bob", [25] = "age", occupation = "Engineer" }: 展示了使用不同类型键的字典。 ["name"] = "Bob" 和 occupation = "Engineer" 使用字符串键,[25] = "age" 使用数字键。 注意,当键是字符串且符合 Lua 标识符命名规则(字母、数字、下划线,且不能以数字开头)时,可以省略引号和方括号,直接写成 键名 = 值 的形式,例如 occupation = "Engineer"。
print("myDict1 name:", myDict1.name, ...) 和 print("myDict2 name:", myDict2["name"], ...): 访问字典元素有两种主要方式:
点号 . 访问: myDict1.name 只能用于键是字符串且符合标识符命名规则的情况,例如 name, age, city。 这种方式更简洁易读。
方括号 [] 访问: myDict2["name"], myDict2[25] 适用于任何类型的键,包括字符串、数字、甚至表等。 当键不是字符串或不符合标识符规则时,必须使用方括号访问。
代码实践 2.2: 添加、修改和删除字典条目
local myDict = { name = "Alice", age = 30 } -- 添加新的键值对 myDict.city = "London" myDict["country"] = "UK" print("添加 city 和 country 后:", myDict.name, myDict.age, myDict.city, myDict.country) -- 输出: 添加 city 和 country 后: Alice 30 London UK -- 修改已有的键的值 myDict.age = 31 myDict["city"] = "Paris" print("修改 age 和 city 后:", myDict.name, myDict.age, myDict.city, myDict.country) -- 输出: 修改 age 和 city 后: Alice 31 Paris UK -- 删除键值对 myDict.country = nil -- 将键的值设置为 nil 即可删除 myDict["age"] = nil print("删除 country 和 age 后:", myDict.name, myDict.age, myDict.city, myDict.country) -- 输出: 删除 country 和 age 后: Alice nil Paris nil
内容详解 2.2:
myDict.city = "London" 和 myDict["country"] = "UK": 添加新的键值对。 如果键不存在,则会创建新的键值对并赋值。
myDict.age = 31 和 myDict["city"] = "Paris": 修改已有的键的值。 如果键已存在,则会更新键对应的值。
myDict.country = nil 和 myDict["age"] = nil: 删除字典中的键值对。 将键对应的值设置为 nil 是从表中删除键的唯一方法。 Lua 中没有专门的删除函数,将值设置为 nil 会自动回收键。
代码实践 2.3: 遍历字典
local myDict = { name = "Alice", age = 30, city = "New York", occupation = "Engineer" } -- 使用 pairs 迭代器遍历字典 print("使用 pairs 迭代器遍历:") for key, value in pairs(myDict) do print("键:", key, "值:", value) end
内容详解 2.3:
for key, value in pairs(myDict) do ... end: pairs 是 Lua 提供的迭代器,专门用于遍历字典类型的表。 它会迭代表中的所有键值对,并返回键和对应的值。 pairs 不保证遍历的顺序,键值对的顺序可能与插入顺序不同,甚至在不同的 Lua 版本或运行环境中都可能不同。 如果需要按照特定的顺序遍历字典,需要使用其他方法,例如将键排序后遍历。代码实践 2.4: 判断键是否存在
local myDict = { name = "Alice", age = 30, city = "New York" } -- 判断键是否存在 if myDict.name ~= nil then print("键 'name' 存在,值为:", myDict.name) end if myDict["city"] ~= nil then print("键 'city' 存在,值为:", myDict["city"]) end if myDict.country == nil then print("键 'country' 不存在") end
内容详解 2.4:
if myDict.name ~= nil then ... end: 判断键 name 是否存在。 在 Lua 中,访问一个不存在的键会返回 nil。 因此,可以通过判断访问键的结果是否为 nil 来判断键是否存在。 注意: 这种方法有一个潜在的陷阱,如果键存在但其对应的值恰好是 nil,则也会被误判为键不存在。 在实际应用中,如果字典的值可能为 nil,需要更严谨的判断方法,例如使用 rawget 函数,但这通常不是必需的。 对于大多数情况,简单的判断 != nil 已经足够。3. 作为记录/结构体 (Records/Structures) 使用
表可以用来模拟面向对象编程中的记录或结构体,将相关的数据和操作方法组织在一起。 这使得 Lua 可以进行面向数据编程,甚至一定程度上模拟面向对象编程的特性。
代码实践 3.1: 创建记录/结构体
-- 创建一个表示点的记录 local Point = { x = 10, y = 20 } -- 创建一个表示矩形的记录 local Rectangle = { width = 100, height = 50, position = Point -- 嵌套记录,矩形的位置是一个点 } -- 打印记录内容 print("Point x:", Point.x, "y:", Point.y) print("Rectangle width:", Rectangle.width, "height:", Rectangle.height) print("Rectangle position x:", Rectangle.position.x, "y:", Rectangle.position.y)
内容详解 3.1:
local Point = { x = 10, y = 20 } 和 local Rectangle = { ... }: 使用表来表示数据结构。 Point 记录包含 x 和 y 坐标,Rectangle 记录包含 width, height 和 position (它本身也是一个 Point 记录,体现了记录的嵌套)。
Rectangle.position = Point: 记录可以嵌套,将一个记录作为另一个记录的字段值。
Rectangle.position.x: 通过链式点号访问嵌套记录的字段。
代码实践 3.2: 为记录添加方法 (函数)
-- 创建一个表示圆的记录,并添加计算面积的方法 local Circle = { radius = 5, -- 方法:计算圆的面积 area = function(self) -- self 参数表示圆自身 return math.pi * self.radius ^ 2 end } -- 调用方法 local circleArea = Circle:area() -- 使用冒号 : 语法糖调用方法,会自动将 Circle 传递给 self 参数 print("圆的面积:", circleArea) -- 输出: 圆的面积: 78.539816339745
内容详解 3.2:
area = function(self) ... end: 在 Circle 记录中添加一个名为 area 的字段,其值是一个匿名函数。 这个函数就成为了 Circle 记录的方法。
self 参数: 方法函数的第一个参数通常命名为 self,它代表调用该方法的记录自身。 在方法内部,可以使用 self.字段名 的方式访问记录自身的其他字段。
Circle:area(): 使用冒号 : 语法糖来调用方法。 Circle:area() 等价于 Circle.area(Circle)。 冒号语法会自动将调用者 Circle 作为第一个参数 (即 self) 传递给 area 方法。 这是一种更简洁、更面向对象风格的方法调用方式。 也可以使用点号 ., 写成 Circle.area(Circle), 但冒号更常用和推荐。
代码实践 3.3: 创建多个记录实例
-- 创建一个创建 Point 记录的工厂函数 local function createPoint(x, y) return { x = x, y = y } end -- 创建两个 Point 记录实例 local point1 = createPoint(1, 2) local point2 = createPoint(5, 10) print("point1 x:", point1.x, "y:", point1.y) -- 输出: point1 x: 1 y: 2 print("point2 x:", point2.x, "y:", point2.y) -- 输出: point2 x: 5 y: 10
内容详解 3.3:
local function createPoint(x, y) ... end: 创建了一个名为 createPoint 的工厂函数。 工厂函数用于创建特定类型的记录实例。 它接受参数 (例如 x, y 坐标),并返回一个新的记录表。
local point1 = createPoint(1, 2) 和 local point2 = createPoint(5, 10): 调用工厂函数 createPoint 创建了两个独立的 Point 记录实例 point1 和 point2。 每个实例都拥有自己的数据 (不同的 x, y 值)。
代码实践 3.4: 更复杂的记录结构 (例如链表节点)
-- 定义链表节点结构 local LinkedListNode = { data = nil, next = nil } -- 创建链表节点 local node1 = { data = 10, next = nil } local node2 = { data = 20, next = nil } local node3 = { data = 30, next = nil } -- 构建链表关系 node1.next = node2 node2.next = node3 -- 遍历链表 local currentNode = node1 print("遍历链表:") while currentNode ~= nil do print(currentNode.data) -- 输出节点数据 currentNode = currentNode.next -- 移动到下一个节点 end
内容详解 3.4:
local LinkedListNode = { data = nil, next = nil }: 定义了链表节点的结构。 data 字段存储节点的数据,next 字段指向下一个节点 (或 nil 表示链表末尾)。 虽然这里定义了 LinkedListNode 表,但实际上 Lua 中创建链表节点时,通常直接创建匿名表即可,结构定义更多是概念上的。
node1.next = node2, node2.next = node3: 通过设置 next 字段,将节点连接起来,构建链表。
链表遍历: 使用 while 循环和 currentNode.next 逐步访问链表中的每个节点,直到 currentNode 变为 nil (到达链表末尾)。
4. 作为集合 (Sets) 使用
集合是一种数据结构,用于存储一组唯一的元素。 Lua 表可以利用其键的唯一性来实现集合的功能。 通常,使用表的键来存储集合元素,而值可以忽略 (或者统一设置为 true)。 只需要关注键的存在与否,来表示元素是否在集合中。