第五章:表的高级应用

表(Table)不仅仅是数据容器,它还是 Lua 模块系统、面向对象编程和元编程的基石。在前面的章节中,我们学习了表的基础操作,本章将深入探讨表的更多高级特性。

我们将学习如何操作表、使用元表(Metatable)改变表的行为,以及如何利用表来实现面向对象编程(OOP)。

5.1 表操作

Lua 的 table 库提供了许多实用的函数来操作数组类型的表。

插入与删除

  • table.insert(list, [pos,] value): 在指定位置插入元素。默认插入末尾。

  • table.remove(list, [pos]): 删除指定位置的元素,并返回该元素。默认删除最后一个。

local t = {10, 20, 30}
table.insert(t, 40)      -- {10, 20, 30, 40}
table.insert(t, 2, 15)   -- {10, 15, 20, 30, 40}
print(table.remove(t, 1)) -- 10, t 变为 {15, 20, 30, 40}

表连接

table.concat(list, [sep, [i, [j]]]): 将数组中的元素连接成字符串。

  • sep: 分隔符(默认为空字符串)。

  • i, j: 起始和结束索引。

local t = {"Lua", "is", "great"}
print(table.concat(t, " ")) -- "Lua is great"

表排序

table.sort(list, [comp]): 对数组进行排序。comp 是可选的比较函数。

local t = {3, 1, 4, 1, 5}
table.sort(t) -- {1, 1, 3, 4, 5}

-- 自定义降序排序
table.sort(t, function(a, b) return a > b end) -- {5, 4, 3, 1, 1}

5.2 元表 (Metatable)

元表是 Lua 中最强大的特性之一。它允许我们修改表的行为,例如定义两个表相加的操作、定义如何访问不存在的键等。

设置和获取元表

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

  • getmetatable(table): 获取表的元表。

元方法 (Metamethods)

元表中包含的特殊键称为元方法。常见的元方法有:

  • __add: + 操作

  • __sub: - 操作

  • __mul: * 操作

  • __div: / 操作

  • __tostring: 用于 printtostring 转换时的输出

  • __index: 当访问不存在的索引时调用

  • __newindex: 当给不存在的索引赋值时调用

  • __call: 当把表当作函数调用时触发

示例:重载加法运算符

local Set = {}
local mt = {}

function Set.new(l)
    local set = {}
    setmetatable(set, mt)
    for _, v in ipairs(l) do set[v] = true end
    return set
end

function Set.union(a, b)
    local res = Set.new{}
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do l[#l + 1] = tostring(e) end
    return "{" .. table.concat(l, ", ") .. "}"
end

mt.__add = Set.union
mt.__tostring = Set.tostring

local s1 = Set.new{10, 20, 30}
local s2 = Set.new{30, 1}
print(s1)       -- 调用 __tostring
print(s1 + s2)  -- 调用 __add, 输出 {1, 10, 20, 30} (顺序不一定)

__index 元方法

这是实现面向对象编程的关键。当访问表 t 中不存在的字段 k 时,如果 t 有元表且元表有 __index 字段:

  1. 如果 __index 是一个表,则在这个表中查找 k

  2. 如果 __index 是一个函数,则调用该函数 __index(t, k)

local proto = {x = 0, y = 0}
local mt = {__index = proto}

local o = {x = 10}
setmetatable(o, mt)

print(o.x) -- 10 (存在)
print(o.y) -- 0  (不存在,去 proto 中找)

5.3 面向对象编程 (OOP)

Lua 没有内置的类(Class)概念,但我们可以使用表和元表来模拟。

类的实现

通常,我们创建一个表作为“类”,并设置 __index 指向它自己。

Account = {balance = 0}

function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function Account:deposit(v)
    self.balance = self.balance + v
end

function Account:withdraw(v)
    if v > self.balance then error"insufficient funds" end
    self.balance = self.balance - v
end

-- 创建对象
a = Account:new{balance = 0}
a:deposit(100.00)
print(a.balance) -- 100.0

冒号语法

a:deposit(100)a.deposit(a, 100) 的语法糖。定义函数时使用 function Account:deposit(v) 会自动添加一个隐藏参数 self

继承

我们可以基于现有类创建新类。

SpecialAccount = Account:new()

function SpecialAccount:withdraw(v)
    if v - self.balance >= self.limit then
        error"insufficient funds"
    end
    self.balance = self.balance - v
end

s = SpecialAccount:new{limit = 1000.00}
s:deposit(100.00)
s:withdraw(200.00)
print(s.balance) -- -100.0

5.4 模块与包

Lua 5.1 引入了模块系统。

编写模块

通常一个文件就是一个模块,模块最后返回一个包含导出函数的表。

-- mymodule.lua
local M = {}

function M.say_hello()
    print("Hello from module!")
end

return M

使用模块

使用 require 函数加载模块。

local m = require("mymodule")
m.say_hello()

Lua 会在 package.path 指定的路径中搜索模块。

练习题

  1. 编写一个函数 Set.intersection(a, b) 计算两个集合的交集,并将其绑定到元方法 __mul (*) 上。

  2. 实现一个 Stack(栈)类,包含 push, pop, peek 方法。

  3. 尝试实现一个只读表:利用 __index__newindex 元方法,使得访问存在的键正常,修改任何键都报错。

  4. 创建一个模块 complex,用于处理复数运算,并支持 +, - 运算符重载。


下一章预告:我们将深入研究 Lua 强大的字符串模式匹配功能,它类似于正则表达式但更加轻量高效。