元表(Metatable)与元方法

11.1 什么是元表

元表(metatable) 是一张特殊的表,它定义了另一个表的行为。通过元表,我们可以:

  • 改变表的算术运算行为(加法、减法等)
  • 控制表的索引和赋值行为
  • 自定义表的打印方式
  • 实现继承、默认值等高级功能
1
2
3
4
5
6
-- 每个表都可以有一个元表
local t = {}
local mt = {}

setmetatable(t, mt) -- 将 mt 设为 t 的元表
print(getmetatable(t)) --> table: 0x...(返回 mt)

元表中定义的元方法(metamethods)__ 开头,例如 __add__index__tostring


11.2 __index 元方法

__index最常用的元方法,当访问表中不存在的键时触发。

作为表的 __index

1
2
3
4
5
6
7
8
9
10
11
12
13
local defaults = {name = "匿名", age = 0, city = "未知"}

local mt = {
__index = defaults -- 当 t 中没有 key 时,去 defaults 中查找
}

local t = setmetatable({}, mt)
t.name = "张三"

print(t.name) --> 张三(表中有,直接返回)
print(t.age) --> 0(表中没有,从 defaults 获取)
print(t.city) --> 未知
print(t.country) --> nil(defaults 中也没有)

作为函数的 __index

1
2
3
4
5
6
7
8
9
10
11
12
13
local mt = {
__index = function(t, key)
print("访问不存在的键:", key)
if key == "timestamp" then
return os.time()
end
return nil
end
}

local t = setmetatable({}, mt)
print(t.name) --> nil(打印:访问不存在的键:name)
print(t.timestamp) --> 当前时间戳

原型继承

这是 __index 最经典的用法——实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local Animal = {
name = "动物",
sound = "..."
}

function Animal:speak()
print(self.sound)
end

function Animal:describe()
print("这是一只" .. self.name)
end

-- 创建子类
local Cat = setmetatable({name = "猫", sound = "喵"}, {__index = Animal})
local Dog = setmetatable({name = "狗", sound = "汪"}, {__index = Animal})

Cat:speak() --> 喵(Cat 中没有 speak,从 Animal 继承)
Dog:speak() --> 汪
Cat:describe() --> 这是一只猫

11.3 __newindex 元方法

__newindex给表中不存在的键赋值时触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
local mt = {
__newindex = function(t, key, value)
print("不允许新增属性:" .. key)
end
}

local t = setmetatable({name = "只读"}, mt)

t.name = "修改" --> 直接修改已有的(不触发 __newindex)
print(t.name) --> 修改

t.age = 25 --> 触发 __newindex,打印:不允许新增属性:age
print(t.age) --> nil(赋值被阻止)

只读表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local function read_only(t)
local proxy = {}

local mt = {
__index = t, -- 读操作从原表读取

__newindex = function(_, key, value)
error("不能修改只读表的属性:" .. key)
end
}

return setmetatable(proxy, mt)
end

local original = {name = "Lua", version = 5.4}
local ro = read_only(original)

print(ro.name) --> Lua
print(ro.version) --> 5.4
-- ro.newKey = "test" --> 报错:不能修改只读表的属性
original.name = "Lua 5.4" --> 原始表仍然可以修改

代理表(Proxy Table)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local function proxy(target)
local p = {}

local mt = {
__index = function(_, key)
print("读取:" .. key)
return target[key]
end,

__newindex = function(_, key, value)
print("写入:" .. key .. " = " .. tostring(value))
target[key] = value
end
}

return setmetatable(p, mt)
end

local data = {x = 0}
local p = proxy(data)

print(p.x) --> 读取:x / 0
p.x = 42 --> 写入:x = 42
print(data.x) --> 42(原表也被修改)

11.4 运算符重载

算术运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
local Vector = {}

function Vector:new(x, y)
return setmetatable({x = x, y = y}, {__index = Vector})
end

function Vector:__add(other)
return Vector:new(self.x + other.x, self.y + other.y)
end

function Vector:__sub(other)
return Vector:new(self.x - other.x, self.y - other.y)
end

function Vector:__mul(scalar)
return Vector:new(self.x * scalar, self.y * scalar)
end

function Vector:__div(scalar)
return Vector:new(self.x / scalar, self.y / scalar)
end

function Vector:__unm()
return Vector:new(-self.x, -self.y)
end

-- 使用
local v1 = Vector:new(1, 2)
local v2 = Vector:new(3, 4)
local v3 = v1 + v2
print(v3.x, v3.y) --> 4 6

local v4 = v1 * 3
print(v4.x, v4.y) --> 3 6

local v5 = -v1
print(v5.x, v5.y) --> -1 -2

关系运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Vector:__eq(other)
return self.x == other.x and self.y == other.y
end

function Vector:__lt(other)
-- 按模长比较
return (self.x^2 + self.y^2) < (other.x^2 + other.y^2)
end

function Vector:__le(other)
return self:__lt(other) or self:__eq(other)
end

local v1 = Vector:new(1, 2)
local v2 = Vector:new(1, 2)
local v3 = Vector:new(3, 4)

print(v1 == v2) --> true
print(v1 < v3) --> true
print(v1 <= v2) --> true

连接与长度

1
2
3
4
5
6
7
8
9
10
11
12
13
function Vector:__concat(other)
return "(" .. self.x .. "," .. self.y .. ")-(" .. other.x .. "," .. other.y .. ")"
end

function Vector:__len()
return math.sqrt(self.x^2 + self.y^2)
end

local v1 = Vector:new(1, 2)
local v2 = Vector:new(3, 4)

print(v1 .. v2) --> (1,2)-(3,4)
print(#v1) --> 3.236...(sqrt(5))

11.5 __tostring__call

__tostring

控制表的字符串表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
local mt = {
__tostring = function(t)
local parts = {}
for k, v in pairs(t) do
table.insert(parts, k .. "=" .. tostring(v))
end
return "{" .. table.concat(parts, ", ") .. "}"
end
}

local t = setmetatable({name = "Lua", version = 5.4, year = 2026}, mt)
print(t) --> {name=Lua, version=5.4, year=2026}
print(tostring(t)) --> 同上

__call

使表可以像函数一样被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local function create_counter(start)
local mt = {
__call = function(t)
t.count = t.count + 1
return t.count
end
}

local counter = setmetatable({count = start or 0}, mt)

counter.reset = function(t)
t.count = start or 0
end

return counter
end

local c = create_counter(10)
print(c()) --> 11(通过 __call 调用)
print(c()) --> 12
print(c()) --> 13

c:reset()
print(c()) --> 11

11.6 __pairs__ipairs

自定义遍历行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
local function sorted_pairs(t)
local keys = {}
for k in pairs(t) do
table.insert(keys, k)
end
table.sort(keys)

local i = 0
return function()
i = i + 1
local k = keys[i]
if k then
return k, t[k]
end
return nil
end
end

local mt = {
__pairs = function(t)
return sorted_pairs(t)
end
}

local t = setmetatable({z = 1, a = 2, m = 3}, mt)

for k, v in pairs(t) do
print(k, v) --> a 2 / m 3 / z 1(按字母序)
end

-- 注意:__pairs 只在 Lua 5.2+ 支持

11.7 __gc 元方法

当表被垃圾回收时触发(仅对 userdata 有效,Lua 5.2+ 对全表有效需 C API):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 在 Lua 层面,__gc 通常用于 userdata
-- 这里展示一个利用弱表的例子:
local resources = setmetatable({}, {__mode = "v"})

local function create_resource(name)
local r = {name = name, valid = true}
table.insert(resources, r)

setmetatable(r, {
__gc = function(obj)
print("回收资源:" .. obj.name)
obj.valid = false
end
})

return r
end

-- 注意:Lua 表的 __gc 需要 Lua 5.4 才完整支持

11.8 综合示例:可定制的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
local function create_flexible_table(data)
-- 合并 __index、__newindex、__tostring、__call
local mt = {
__index = function(t, key)
if key == "type" then return "flexible_table" end
if key == "keys" then
local ks = {}
for k in pairs(t) do table.insert(ks, k) end
return ks
end
return rawget(t, key)
end,

__newindex = function(t, key, value)
if key == "locked" then
rawset(t, key, value)
return
end
if t.locked then
error("表已锁定,不能修改")
end
rawset(t, key, value or "nil")
end,

__tostring = function(t)
local parts = {}
for k, v in pairs(t) do
if k ~= "locked" then
table.insert(parts, k .. "=" .. tostring(v))
end
end
return "[" .. table.concat(parts, ", ") .. "]"
end,

__call = function(t, action)
if action == "clear" then
for k in pairs(t) do
if k ~= "locked" then
rawset(t, k, nil)
end
end
elseif action == "count" then
local n = 0
for k in pairs(t) do
if k ~= "locked" then n = n + 1 end
end
return n
end
end
}

return setmetatable(data or {}, mt)
end

local ft = create_flexible_table({a = 1, b = 2})
print(ft.type) --> flexible_table
print(ft("count")) --> 2
ft.locked = true
print(ft.keys) --> a, b, locked

11.9 本章小结

元方法 触发时机
__index 访问不存在的键
__newindex 给不存在的键赋值
__add / __sub / __mul / __div 算术运算
__eq / __lt / __le 关系运算
__concat 连接运算 ..
__len 长度运算 #
__unm 负号 -
__tostring tostring()
__call 像函数一样调用
__pairs / __ipairs 遍历

元表是 Lua 最强大的特性之一,它是实现面向对象、继承、运算符重载、代理模式等高级功能的基础。

练习题

  1. 实现一个 ProtectedTable,记录每次访问的时间戳
  2. __call 实现一个简单的函数式复数库(支持加减乘除)
  3. __tostring 实现格式化输出树形数据结构
  4. 实现一个 LazyTable,在首次访问属性时才计算值并缓存
  5. __index 实现”缺失键自动生成”的自动增长数组
ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
Lua基础教程 系列
第 11/15 篇
分享: