强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Lua 从入门到精通 / 06 - 表 / Tables

表 / Tables

Table 是 Lua 中唯一的数据结构。数组、字典、对象、模块、命名空间——全部用 table 实现。理解 table,就理解了 Lua 的一半。

Table is Lua’s only data structure. Arrays, dicts, objects, modules, namespaces — all implemented with tables. Understand tables, understand half of Lua.


🟢 基础 / Basics — 创建和使用 Table

1. 创建 Table

-- 空表
local t = {}

-- 数组式(序列)/ Array (sequence)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1])    -- "apple"  ← 索引从 1 开始!

-- 字典式 / Dictionary
local person = {
    name = "Alice",
    age = 30,
    city = "Beijing",
}
print(person.name)       -- "Alice"
print(person["age"])     -- 30(点号是中括号的语法糖)

-- 混合式 / Mixed
local data = {
    "first",           -- data[1]
    "second",          -- data[2]
    x = 100,           -- data["x"]
    y = 200,           -- data["y"]
    [true] = "yes",    -- data[true]  ← 键可以是任何非 nil 值
    [function() end] = "fn",  -- 甚至可以是函数!
}

2. 访问与修改 / Access & Modify

local t = {name = "Lua", version = 5.4}

-- 读取
print(t.name)        -- "Lua"
print(t["version"])  -- 5.4
print(t.missing)     -- nil(不存在的键返回 nil)

-- 赋值 / 修改
t.version = 5.4
t["new key"] = "hello"    -- 键可以有空格
t[1] = "first"

-- 删除:赋值为 nil
t.name = nil    -- 等同于删除了 "name" 键

3. 遍历 / Iteration

local t = {name = "Lua", version = 5.4, year = 1993}

-- pairs:遍历所有键值对(顺序不确定)
for k, v in pairs(t) do
    print(k, v)
end

-- ipairs:从 1 开始遍历连续的整数键(遇到 nil 停止)
local arr = {10, 20, 30, nil, 50}
for i, v in ipairs(arr) do
    print(i, v)    -- 只输出 1,2,3(遇到 nil 停止)
end

-- # 运算符:序列的长度
local fruits = {"apple", "banana", "cherry"}
print(#fruits)    -- 3

4. Table 作为函数参数 / Table as Function Argument

-- 常见模式:用 table 模拟命名参数
local function createWindow(options)
    local width = options.width or 640
    local height = options.height or 480
    local title = options.title or "Window"
    print(string.format("%s: %dx%d", title, width, height))
end

createWindow({
    width = 800,
    title = "My App",
})

-- 当 table 是唯一参数时,可以省略括号
createWindow{width = 1024, height = 768, title = "Big"}

🟡 进阶 / Intermediate — Table 的实用模式

1. 序列操作 / Sequence Operations

-- table.insert:在指定位置插入
local t = {1, 2, 3}
table.insert(t, 4)         -- {1, 2, 3, 4}  尾部插入
table.insert(t, 2, 99)     -- {1, 99, 2, 3, 4}  在位置 2 插入

-- table.remove:移除指定位置元素
local t = {1, 2, 3, 4, 5}
table.remove(t, 2)         -- 返回 2, t = {1, 3, 4, 5}
table.remove(t)            -- 返回 5, t = {1, 3, 4}(移除最后一个)

-- table.sort:排序
local t = {3, 1, 4, 1, 5, 9, 2, 6}
table.sort(t)
-- t = {1, 1, 2, 3, 4, 5, 6, 9}

-- 自定义排序
table.sort(t, function(a, b) return a > b end)    -- 降序

-- table.concat:连接为字符串
local t = {"hello", "world", "lua"}
print(table.concat(t, " "))    -- "hello world lua"
print(table.concat(t, ", "))   -- "hello, world, lua"

2. table 作为数组的陷阱 / Array Pitfalls

-- 陷阱 1:不要对非序列使用 #
local t = { [1] = "a", [3] = "c" }
print(#t)    -- 1(不是 2!遇到 nil 停止)

-- 陷阱 2:nil 洞会破坏 #
local t = {1, 2, nil, 4, 5}
print(#t)    -- 2 或 5(行为未定义!不要这样做)

-- 陷阱 3:pairs 不保证顺序
local t = {a=1, b=2, c=3}
for k, v in pairs(t) do
    print(k)    -- 顺序不确定!
end

-- 正确做法:需要有序遍历时,手动构建键数组
local t = {a=1, b=2, c=3}
local keys = {}
for k in pairs(t) do keys[#keys+1] = k end
table.sort(keys)
for _, k in ipairs(keys) do
    print(k, t[k])
end
-- a 1
-- b 2
-- c 3

3. Table 复制 / Table Copy

-- 浅拷贝:只复制第一层
local function shallowCopy(t)
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = v
    end
    return copy
end

-- 深拷贝:递归复制所有层级
local function deepCopy(t, seen)
    if type(t) ~= "table" then return t end
    if seen and seen[t] then return seen[t] end    -- 处理循环引用
    
    seen = seen or {}
    local copy = {}
    seen[t] = copy    -- 先记录,再递归(防止循环引用)
    
    for k, v in pairs(t) do
        copy[deepCopy(k, seen)] = deepCopy(v, seen)
    end
    return setmetatable(copy, getmetatable(t))
end

-- 测试深拷贝
local original = {a = {x = 1}, b = {y = 2}}
original.self = original    -- 循环引用!

local copied = deepCopy(original)
print(copied.a.x)           -- 1
print(copied.self == copied) -- true(循环引用被正确处理)

4. Table 合并 / Table Merge

-- 浅合并:将 src 的字段复制到 dst
local function merge(dst, src)
    for k, v in pairs(src) do
        dst[k] = v
    end
    return dst
end

-- 使用场景:默认配置 + 用户配置
local defaults = {host="localhost", port=8080, debug=false}
local userConfig = {port=3000, debug=true}

local config = merge({}, defaults)
merge(config, userConfig)
-- config = {host="localhost", port=3000, debug=true}

🔴 高级 / Advanced — Table 的内部实现

1. 双重存储:数组部分 + 哈希部分

Lua table 内部结构 / Internal structure:

local t = {10, 20, 30, x="hello", y="world"}

┌──────────────────────────────────────────┐
│                 Table                     │
├─────────────────┬────────────────────────┤
│  Array Part     │   Hash Part            │
│  (序列化整数键) │   (其他所有键)          │
├─────────────────┼────────────────────────┤
│  [1] = 10       │   "x"  ► "hello"      │
│  [2] = 20       │   "y"  ► "world"      │
│  [3] = 30       │                        │
├─────────────────┴────────────────────────┤
│  metatable = nil                          │
│  flags = ...                              │
└──────────────────────────────────────────┘

何时使用数组部分?何时使用哈希部分?

  • 整数键 1~n 的连续序列 → 存入数组部分(O(1) 直接索引)
  • 其他键(字符串、不连续整数等)→ 存入哈希部分(O(1) 哈希查找)

2. Rehash 策略

-- 当 table 需要扩容时,Lua 会 rehash
-- Lua uses a power-of-2 growth strategy

-- 插入新键导致 rehash:
local t = {}
for i = 1, 100 do
    t[i] = i
end
-- rehash 发生在 1, 2, 4, 8, 16, 32, 64, 128...
-- 每次 rehash 都是 O(n),但分摊后每次插入是 O(1)

-- 性能提示:预分配 table 大小
local t = table.new(100, 0)    -- LuaJIT 特有
-- 或
local t = {}
for i = 1, 100 do t[i] = false end    -- 预填充
for i = 1, 100 do t[i] = i end        -- 实际赋值

3. 弱引用表 / Weak Tables

-- 弱引用表:键或值被弱引用,GC 时可被回收
-- Weak table: keys or values are weakly referenced

-- "k" = 键弱引用,"v" = 值弱引用,"kv" = 都弱引用
local mt = {__mode = "v"}    -- 值弱引用
local weak = setmetatable({}, mt)

local obj = {data = "important"}
weak[1] = obj    -- weak 持有 obj 的弱引用

print(weak[1])   -- {data = "important"}

obj = nil        -- 解除强引用
collectgarbage() -- 触发 GC
print(weak[1])   -- nil!obj 被 GC 回收了

-- 应用场景 1:对象缓存(不阻止 GC)
local cache = setmetatable({}, {__mode = "v"})
local function getObject(id)
    if cache[id] then return cache[id] end
    local obj = createExpensiveObject(id)
    cache[id] = obj
    return obj
end

-- 应用场景 2:观察者模式(不阻止 GC)
local listeners = setmetatable({}, {__mode = "k"})
function addListener(obj, callback)
    listeners[obj] = callback
end
-- 当 obj 被 GC 后,对应的 listener 自动移除

4. 只读表 / Read-Only Table

-- 利用 __newindex 元方法实现只读表
local function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(_, k, v)
            error("Attempt to modify read-only table", 2)
        end,
        __pairs = function() return pairs(t) end,
        __len = function() return #t end,
    }
    return setmetatable(proxy, mt)
end

local config = readOnly({host="localhost", port=8080})
print(config.host)    -- "localhost"
-- config.port = 3000  -- Error: Attempt to modify read-only table

小结 / Summary

层级你需要知道的 / What You Need to Know
🟢 基础创建 table、[]/. 访问、索引从 1 开始、pairs/ipairs、# 运算符
🟡 进阶table.insert/remove/sort/concat、浅拷贝/深拷贝、合并、命名参数模式
🔴 高级数组部分+哈希部分、rehash 策略、弱引用表、只读表实现

下一章:字符串与模式匹配 / Strings & Patterns