返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 11 章 元表与元方法

第 11 章 元表与元方法

元表定义了表在特定操作下的行为。访问不存在的键、执行加法、比较两个表或连接字符串时,Lua 会检查相应元方法。借助元表可以实现默认值、继承、代理对象和运算符重载。

元表基础

每个表可以关联另一个表作为元表:

local object = {}
local meta = {}

setmetatable(object, meta)
print(getmetatable(object) == meta) -- true

元方法是在元表中具有固定名称的字段,例如 __index、__add、__tostring。普通字段不会自动生效:

local point = { x = 1, y = 2 }

setmetatable(point, {
  __tostring = function(value)
    return string.format("Point(%d, %d)", value.x, value.y)
  end
})

print(tostring(point)) -- Point(1, 2)

setmetatable 返回传入的第一个表。除非通过 __metatable 保护,否则 getmetatable 可以读取元表。

算术与比较元方法

算术运算

local Vector = {}
Vector.__index = Vector

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

Vector.__add = function(left, right)
  return Vector.new(left.x + right.x, left.y + right.y)
end

Vector.__mul = function(vector, scalar)
  return Vector.new(vector.x * scalar, vector.y * scalar)
end

local result = Vector.new(1, 2) + Vector.new(3, 4)
print(result.x, result.y) -- 4 6

local scaled = Vector.new(1, 2) * 3
print(scaled.x, scaled.y) -- 3 6

Lua 5.4 常见的算术元方法包括:

元方法 操作
__add +
__sub -
__mul *
__div /
__idiv //
__mod %
__pow ^
__unm 一元 -
__band、__bor、__bxor 位运算
__bnot、__shl、__shr 一元按位非与移位

二元元方法会先看左操作数的元表,再尝试右操作数。两个值的类型和元方法设计应在文档中明确,否则重载会让代码难以推理。

比较运算

local Version = {}
Version.__index = Version

function Version.new(major, minor)
  return setmetatable({ major = major, minor = minor }, Version)
end

Version.__eq = function(left, right)
  return left.major == right.major and left.minor == right.minor
end

Version.__lt = function(left, right)
  if left.major == right.major then
    return left.minor < right.minor
  end
  return left.major < right.major
end

local first = Version.new(5, 4)
local second = Version.new(5, 4)
local third = Version.new(5, 5)

print(first == second) -- true
print(first < third)   -- true

实现了 __lt 和 __eq 后,可以定义 __le 提供小于等于行为。比较元方法不应产生副作用,也应保持等价关系一致。

__index 与继承

访问表中不存在的键时,Lua 会读取元表的 __index。它既可以是表,也可以是函数:

local defaults = {
  host = "127.0.0.1",
  port = 8080
}

local config = setmetatable({}, {
  __index = defaults
})

print(config.host) -- 127.0.0.1
config.port = 9090
print(config.port) -- 9090
print(defaults.port) -- 8080

函数形式的 __index 可以记录访问或生成默认值:

local lazyConfig = setmetatable({}, {
  __index = function(target, key)
    local value = "generated:" .. key
    rawset(target, key, value)
    return value
  end
})

rawget 和 rawset 绕过元方法,适合实现底层逻辑:

print(rawget(config, "host")) -- nil

多层原型链

local base = { baseMethod = function() return "base" end }
local middle = setmetatable({}, { __index = base })
local leaf = setmetatable({}, { __index = middle })

print(leaf.baseMethod()) -- base

原型链过深会让查找和调试变复杂。工程中建议控制层级,并避免循环引用。

__newindex 与访问拦截

当给表中不存在的键赋值时会调用 __newindex。已有的键仍直接赋值,除非对象是代理:

local source = {}
local proxy = setmetatable({}, {
  __index = source,
  __newindex = function(_, key, value)
    source[key] = value
  end
})

proxy.name = "Lua"
print(source.name) -- Lua

函数形式可以实现字段校验:

local function createSettings()
  local values = {}
  local allowed = { timeout = true, retries = true }

  return setmetatable({}, {
    __index = values,
    __newindex = function(_, key, value)
      if not allowed[key] then
        error("unknown setting: " .. tostring(key), 2)
      end
      values[key] = value
    end
  })
end

__call 与 __tostring

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

local counter = setmetatable({ value = 0 }, {
  __call = function(self, step)
    self.value = self.value + (step or 1)
    return self.value
  end
})

print(counter())  -- 1
print(counter(5)) -- 6

__tostring 应返回字符串,供 tostring、日志和错误消息使用。不要让 __tostring 递归调用自身或抛出新的复杂错误。

只读表与代理对象

代理表可以把真实数据隐藏起来,同时提供受控访问:

local function readonly(data)
  return setmetatable({}, {
    __index = data,
    __newindex = function()
      error("attempt to modify read-only table", 2)
    end
  })
end

local settings = readonly({ host = "localhost" })
print(settings.host)
-- settings.host = "example.com" -- 抛出错误

这只保护通过代理进行的写入。若调用者仍能拿到原始 data,就可以绕过代理。因此构造函数应只暴露代理。

保护元表

设置 __metatable 后,普通代码无法替换或读取元表:

local protected = setmetatable({}, {
  __metatable = "locked"
})

print(getmetatable(protected)) -- locked
-- setmetatable(protected, {}) -- 抛出错误

它只能提高误用成本,不是安全边界。Lua 沙箱必须同时限制可调用的库和函数。

上一章:模块与包 | 下一章:面向对象 Lua