Lua


6. 表 (Table)


文档摘要

表 (Table) Lua 表 (Table) 详解:代码实践与深度解析 表 (Table) 是 Lua 语言中最核心、最强大的数据结构。它不仅仅是简单的数组或哈希表,而是一种灵活、动态的结构,可以用来表示各种数据类型,从简单的列表到复杂的数据结构,甚至可以模拟类和对象。理解和熟练运用表是掌握 Lua 编程的关键所在。 1. 表的基础概念 在 Lua 中,表本质上是一个关联数组,也称为哈希表或字典。 这意味着表可以存储键值对 (key-value pairs),其中键和值可以是 Lua 中的任何数据类型,除了 。 1.1 表的创建 Lua 提供了两种创建表的主要方式: 字面量构造器 : 这是最常用且简洁的方式。

6. 表 (Table)

Lua 表 (Table) 详解:代码实践与深度解析

表 (Table) 是 Lua 语言中最核心、最强大的数据结构。它不仅仅是简单的数组或哈希表,而是一种灵活、动态的结构,可以用来表示各种数据类型,从简单的列表到复杂的数据结构,甚至可以模拟类和对象。理解和熟练运用表是掌握 Lua 编程的关键所在。

1. 表的基础概念

在 Lua 中,表本质上是一个关联数组,也称为哈希表字典。 这意味着表可以存储键值对 (key-value pairs),其中键和值可以是 Lua 中的任何数据类型,除了 nil

1.1 表的创建

Lua 提供了两种创建表的主要方式:

  • 字面量构造器 {}: 这是最常用且简洁的方式。

    -- 创建一个空表 local emptyTable = {} -- 创建带有初始值的表 local myTable = { key1 = "value1", key2 = 123, [3] = true, -- 使用数字作为键 function() print("Hello from table") end -- 使用函数作为值 }
  • table.new() 函数 (较少使用): 虽然 Lua 标准库并没有 table.new() 函数,但在某些特定的 Lua 环境或库中可能会提供,用于创建特定配置的表。 在标准 Lua 中,我们通常直接使用字面量构造器。

1.2 表的键和值

  • 键 (Key): 可以是除了 nil 以外的任何 Lua 数据类型,最常见的键类型是字符串和数字。 使用字符串作为键时,如果键是合法的 Lua 标识符 (字母、数字、下划线开头,不能是数字开头),可以省略引号,如 key1 = "value1" 等同于 ["key1"] = "value1"。 使用数字或非标识符字符串作为键时,必须使用方括号 [] 包裹,例如 [3] = true["my key"] = "value"

  • 值 (Value): 可以是任何 Lua 数据类型,包括 nil。 将值设置为 nil 相当于从表中删除对应的键。

1.3 表的索引和访问

访问表中的元素使用方括号 [] 或点号 . (当键为字符串且是合法标识符时)。

local myTable = { name = "Lua Table", version = 5.4, [1] = "first element", [2] = "second element" } -- 使用方括号访问 print(myTable["name"]) --> 输出: Lua Table print(myTable[1]) --> 输出: first element print(myTable["version"]) --> 输出: 5.4 -- 使用点号访问 (仅限字符串且为合法标识符的键) print(myTable.name) --> 输出: Lua Table print(myTable.version) --> 输出: 5.4 -- 访问不存在的键会返回 nil print(myTable["nonexistent_key"]) --> 输出: nil

注意:

  • Lua 表的索引从 1 开始,而不是像 C 或 Python 等语言从 0 开始。

  • 使用点号 . 访问时,键必须是字符串且为合法的 Lua 标识符。

  • 访问表中不存在的键不会报错,而是返回 nil

2. 表作为数组 (Array/List)

由于 Lua 表的键可以是数字,因此可以很方便地用表来模拟数组或列表。 当键是连续的整数时,表可以高效地存储和访问序列数据。

2.1 创建数组

-- 创建一个数组 local myArray = { "apple", "banana", "orange" } -- 另一种创建方式,显式指定数字键 local myArray2 = { [1] = "apple", [2] = "banana", [3] = "orange" }

2.2 数组的访问和遍历

可以使用数字索引访问数组元素,并使用 for 循环或 ipairs 迭代器遍历数组。

local myArray = { "apple", "banana", "orange", "grape" } -- 访问数组元素 print(myArray[1]) --> 输出: apple print(myArray[3]) --> 输出: orange -- 使用 for 循环遍历数组 (注意:如果数组中有 nil 值,可能会提前结束循环) for i = 1, #myArray do -- '#' 运算符获取数组的长度 (从 1 开始连续数字键的最大值) print(i, myArray[i]) end --> 输出: --> 1 apple --> 2 banana --> 3 orange --> 4 grape -- 使用 ipairs 迭代器遍历数组 (更安全,只遍历连续数字键) for index, value in ipairs(myArray) do print(index, value) end --> 输出: (与上面的 for 循环输出相同) -- 修改数组元素 myArray[2] = "pear" print(myArray[2]) --> 输出: pear -- 添加元素到数组末尾 myArray[#myArray + 1] = "mango" -- #myArray + 1 是下一个可用索引 print(myArray[5]) --> 输出: mango

2.3 数组的长度 (# 运算符)

# 运算符用于获取表的长度,当表被用作数组时,它返回最大的正整数键值,且这些键值对应的都是非 nil 值。 简单来说,它返回数组中连续数字索引的最后一个索引值。

重要注意:

  • # 运算符只适用于数组风格的表,即键是连续正整数的表。 如果表中有非数字键,或者数字键不连续,# 运算符的结果可能不是你期望的数组元素个数。

  • 如果数组中有 nil 值,并且 nil 值出现在连续数字键的中间,# 运算符可能会提前结束计数。 例如: {1, nil, 3} 的长度可能是 1,而不是 3。 为了避免这种歧义,建议数组中不要包含 nil 值,或者使用 ipairs 进行安全遍历。

3. 表作为字典 (Dictionary/Hash Map)

Lua 表的键可以是字符串或其他非数字类型,这使得它可以像字典或哈希表一样使用,用于存储键值对数据。

3.1 创建字典

-- 创建一个字典 local myDict = { name = "John Doe", age = 30, city = "New York" } -- 另一种创建方式 local myDict2 = {} myDict2["name"] = "Jane Doe" myDict2["age"] = 25 myDict2["city"] = "London"

3.2 字典的访问和遍历

可以使用字符串键或方括号访问字典元素,并使用 pairs 迭代器遍历字典。

local myDict = { name = "John Doe", age = 30, city = "New York" } -- 访问字典元素 print(myDict.name) --> 输出: John Doe print(myDict["age"]) --> 输出: 30 -- 使用 pairs 迭代器遍历字典 (遍历顺序不保证) for key, value in pairs(myDict) do print(key, value) end --> 输出: (顺序可能不同) --> age 30 --> city New York --> name John Doe -- 修改字典元素 myDict.age = 31 print(myDict.age) --> 输出: 31 -- 添加新的键值对 myDict.job = "Engineer" print(myDict.job) --> 输出: Engineer -- 删除键值对 (将值设置为 nil) myDict.city = nil print(myDict.city) --> 输出: nil (键 "city" 实际上已经被移除)

3.3 pairs 迭代器

pairs(table) 是 Lua 提供的一个迭代器函数,用于遍历表中的所有键值对。 与 ipairs 不同,pairs 会遍历表中所有的键值对,包括数字键和非数字键,并且遍历的顺序是不确定的pairs 更适合用于遍历字典类型的表。

4. 表操作函数 (Table Library)

Lua 标准库的 table 模块提供了一系列用于操作表的函数。 常用函数包括:

4.1 table.insert (table, [pos,] value):value 插入到 table 的指定位置 pos。 如果省略 pos,则将 value 插入到表的末尾 (作为数组使用时,插入到数组末尾)。 插入操作会移动 pos 位置及其之后的所有元素,为新元素腾出空间。

local myArray = { "apple", "banana" } table.insert(myArray, "orange") -- 插入到末尾 print(table.concat(myArray, ", ")) --> 输出: apple, banana, orange table.insert(myArray, 2, "pear") -- 插入到索引 2 的位置 print(table.concat(myArray, ", ")) --> 输出: apple, pear, banana, orange

4.2 table.remove (table, [pos]): 移除并返回 table 中指定位置 pos 的元素。 如果省略 pos,则移除并返回表的最后一个元素 (作为数组使用时,移除数组末尾元素)。 移除操作会移动 pos 位置之后的所有元素,填补被移除元素留下的空位。

local myArray = { "apple", "pear", "banana", "orange" } local removedElement = table.remove(myArray, 2) -- 移除索引 2 的元素 print(removedElement) --> 输出: pear print(table.concat(myArray, ", ")) --> 输出: apple, banana, orange local lastElement = table.remove(myArray) -- 移除最后一个元素 print(lastElement) --> 输出: orange print(table.concat(myArray, ", ")) --> 输出: apple, banana

4.3 table.sort (table, [comp]): 对表中的数组元素进行排序。 comp 是一个可选的比较函数,用于自定义排序规则。 如果省略 comp,则默认使用小于号 < 进行升序排序。 注意: table.sort 只对数组部分 (连续数字键部分) 进行排序。

local myArray = { 3, 1, 4, 1, 5, 9, 2, 6 } table.sort(myArray) -- 默认升序排序 print(table.concat(myArray, ", ")) --> 输出: 1, 1, 2, 3, 4, 5, 6, 9 -- 自定义降序排序函数 local function compareDescending(a, b) return a > b end table.sort(myArray, compareDescending) print(table.concat(myArray, ", ")) --> 输出: 9, 6, 5, 4, 3, 2, 1, 1 -- 对字符串数组排序 (默认字典序) local stringArray = { "banana", "apple", "orange" } table.sort(stringArray) print(table.concat(stringArray, ", ")) --> 输出: apple, banana, orange

4.4 table.concat (table, [sep, [i, [j]]]): 连接表中的数组元素为一个字符串。 sep 是分隔符字符串 (可选,默认为空字符串)。 ij 是可选的起始和结束索引,用于指定连接的元素范围 (默认为连接所有数组元素)。

local myArray = { "apple", "banana", "orange" } local concatenatedString = table.concat(myArray, ", ") print(concatenatedString) --> 输出: apple, banana, orange local partialString = table.concat(myArray, "-", 1, 2) -- 连接索引 1 到 2 的元素 print(partialString) --> 输出: apple-banana

4.5 table.move (a1, f, e, t, a2) (Lua 5.3+): 将表 a1 中索引从 fe 的元素移动到表 a2 的索引 t 开始的位置。 a2 可以与 a1 是同一个表。 这是一个高效的批量移动元素的操作。

local sourceTable = { 1, 2, 3, 4, 5 } local targetTable = { 10, 20, 30 } table.move(sourceTable, 2, 4, 2, targetTable) -- 将 sourceTable 中索引 2 (元素 2) 到 4 (元素 4) 的元素 (2, 3, 4) -- 移动到 targetTable 中索引 2 开始的位置。 print(table.concat(sourceTable, ", ")) --> 输出: 1, 5 (元素 2, 3, 4 被移除) print(table.concat(targetTable, ", ")) --> 输出: 10, 2, 3, 4, 30 (元素 2, 3, 4 被插入到 targetTable)

5. 表的深拷贝与浅拷贝

当表作为值赋给另一个变量或作为参数传递给函数时,Lua 默认进行的是浅拷贝 (shallow copy)。 这意味着新的变量或函数参数只是指向同一个表的引用,而不是创建表的副本。 修改其中一个表会影响到另一个表。

local table1 = { name = "Table A", value = 10 } local table2 = table1 -- 浅拷贝 table2.value = 20 -- 修改 table2 print(table1.value) --> 输出: 20 (table1 也被修改了,因为它们指向同一个表)

如果需要创建表的深拷贝 (deep copy),即创建一个完全独立的副本,需要手动实现递归拷贝函数。 深拷贝会递归地复制表及其嵌套的子表,确保修改副本不会影响到原始表。

-- 深拷贝函数 function deepcopy(orig) local copy = {} for key, val in pairs(orig) do if type(val) == "table" then copy[key] = deepcopy(val) -- 递归拷贝子表 else copy[key] = val end end return copy end local table1 = { name = "Table A", data = { x = 1, y = 2 } } local table2 = deepcopy(table1) -- 深拷贝 table2.name = "Table B" table2.data.x = 100 print(table1.name) --> 输出: Table A (table1 未被修改) print(table1.data.x) --> 输出: 1 (table1 未被修改) print(table2.name) --> 输出: Table B print(table2.data.x) --> 输出: 100

6. 表与元表 (Metatable) 和元方法 (Metamethod)

Lua 表的强大之处还在于它可以与 元表 (metatable) 关联,并定义 元方法 (metamethod)。 元表允许我们自定义表的行为,例如:

  • 索引操作 (__index 元方法): 当访问表中不存在的键时,可以委托给元表的 __index 元方法处理。

  • 赋值操作 (__newindex 元方法): 当向表中不存在的键赋值时,可以委托给元表的 __newindex 元方法处理。

  • 算术运算元方法 (__add, __sub, __mul, __div, 等): 可以重载表的算术运算符。

  • 关系运算元方法 (__eq, __lt, __le, 等): 可以重载表的关系运算符。

  • 函数调用元方法 (__call 元方法): 可以将表作为函数调用。

  • 字符串转换元方法 (__tostring 元方法): 自定义表转换为字符串时的行为。

6.1 设置元表 (setmetatable) 和获取元表 (getmetatable)

  • setmetatable(table, metatable): 将 metatable 设置为 table 的元表。

  • getmetatable(table): 返回 table 的元表,如果没有元表则返回 nil

local myTable = {} local myMetatable = {} setmetatable(myTable, myMetatable) print(getmetatable(myTable) == myMetatable) --> 输出: true

6.2 __index 元方法

__index 元方法用于处理索引访问。 当访问 table[key],且 table 中不存在 key 时,Lua 会检查 table 的元表是否有 __index 元方法。

  • 如果 __index 是一个函数,Lua 会调用这个函数 __index(table, key) 并返回函数的结果。

  • 如果 __index 是一个,Lua 会在这个表中查找 key 并返回结果。

local prototype = { defaultName = "Unknown", greet = function(self) print("Hello, my name is " .. self.name) end } local myTable = { name = "Alice" } local metatable = { __index = prototype } setmetatable(myTable, metatable) print(myTable.name) --> 输出: Alice (myTable 自身有 "name") print(myTable.defaultName) --> 输出: Unknown (myTable 自身没有 "defaultName",从 prototype 中查找) myTable:greet() --> 输出: Hello, my name is Alice (myTable 自身没有 "greet",从 prototype 中查找)

6.3 __newindex 元方法

__newindex 元方法用于处理索引赋值。 当执行 table[key] = value,且 table 中不存在 key 时,Lua 会检查 table 的元表是否有 __newindex 元方法。

  • 如果 __newindex 是一个函数,Lua 会调用这个函数 __newindex(table, key, value),由函数来决定如何处理赋值操作。

  • 如果 __newindex 是一个,Lua 会将 value 赋值给元表 __newindex 表的 key 键。 注意: 此时并不会修改原始表 table

local readOnlyTable = {} local metatable = { __newindex = function(table, key, value) print("Attempt to set read-only table at key: " .. key .. ", value: " .. value) -- 可以选择忽略赋值,或者抛出错误 end } setmetatable(readOnlyTable, metatable) readOnlyTable.name = "Bob" --> 输出: Attempt to set read-only table at key: name, value: Bob print(readOnlyTable.name) --> 输出: nil (赋值操作被 __newindex 阻止)

6.4 其他元方法

Lua 提供了丰富的元方法,可以自定义表的各种行为,例如:

  • 算术元方法: __add, __sub, __mul, __div, __mod, __pow, __unm (负号), __concat (字符串连接)。

  • 关系元方法: __eq (等于), __lt (小于), __le (小于等于)。

  • 其他元方法: __call (函数调用), __tostring (字符串转换), __metatable (保护元表), __mode (弱引用表)。

通过元表和元方法,可以实现面向对象编程中的类和对象、运算符重载、访问控制等高级特性。

7. 表的应用场景

Lua 表的灵活性使其在各种场景中都有广泛的应用:

  • 数据存储: 作为数组、列表、字典、集合等数据结构,存储各种类型的数据。

  • 配置管理: 使用表来表示配置文件,易于读取和修改。

  • 面向对象编程: 使用表和元表模拟类和对象,实现面向对象编程。

  • 数据结构: 构建复杂的数据结构,如树、图等。

  • 模块和命名空间: 使用表来组织代码,创建模块和命名空间。

  • 函数库: Lua 标准库和各种扩展库都大量使用表来组织函数和数据。

8. 最佳实践和注意事项

  • 选择合适的表类型: 根据数据结构和访问模式选择使用数组风格的表还是字典风格的表。

  • 避免数组中的 nil 值: 在数组中使用 nil 值可能导致 # 运算符和 for 循环行为不符合预期。 如果需要表示空值,可以考虑使用特殊标记值或使用字典。

  • 理解浅拷贝和深拷贝: 在复制表时,要根据需求选择浅拷贝或深拷贝,避免意外的修改影响。

  • 合理使用元表和元方法: 元表和元方法是强大的工具,但过度使用可能会增加代码的复杂性,要根据实际需求谨慎使用。

  • 性能考虑: Lua 表的性能通常很好,但在处理大量数据时,仍然需要考虑性能优化,例如预先分配表的大小、避免频繁的插入和删除操作等。

9. 总结

Lua 表是 Lua 语言的核心,它集成了数组、字典、对象等多种数据结构的特性,具有高度的灵活性和强大的功能。 掌握 Lua 表的各种用法,包括创建、访问、操作、遍历、元表和元方法等,是成为优秀的 Lua 程序员的必要条件。 通过本文的学习和代码实践,相信你已经对 Lua 表有了更深入的理解,并能够在实际开发中灵活运用。


发布者: 作者: 转发
评论区 (0)
U