0Pricing
Lua Academy · 课时

算术元方法

使用元方法重载 +、-、*、/ 及其他运算符。

算术元方法 是 CoddyKit 上的免费 Lua Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Lua Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Lua Academy 课程共包含 4 节课。

元方法名称

Lua 为所有算术运算符定义了元方法。当 Lua 对一个表执行算术运算时,会在该表的元表中查找相应的元方法。如果两个操作数都是表,Lua 会先检查左操作数的元表。

-- Operator -> Metamethod
-- a + b    -> __add
-- a - b    -> __sub
-- a * b    -> __mul
-- a / b    -> __div
-- a % b    -> __mod
-- a ^ b    -> __pow
-- a // b   -> __idiv
-- -a       -> __unm (unary minus)
-- #a       -> __len
print("Metamethods defined for all Lua operators")

使用 __add 实现向量加法

在向量表上定义 __add,即可支持 + 运算符。元方法会接收两个操作数,并返回一个新值。两个操作数可能都是表,也可能其中一个是数字,因此您的元方法应当处理这两种情况。

local Vec = {}
Vec.__index = Vec

Vec.__add = function(a, b)
  return setmetatable({x=a.x+b.x, y=a.y+b.y}, Vec)
end

Vec.__tostring = function(v)
  return string.format("Vec(%g,%g)", v.x, v.y)
end

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

local v1 = Vec.new(1,2)
local v2 = Vec.new(3,4)
print(tostring(v1 + v2))   -- Vec(4,6)

__sub、__mul、__div

以类似方式定义 __sub、__mul 和 __div。对于乘法,请在元方法内部检查操作数类型,以同时处理表*表和表*数字的情况。

local Vec = {}
Vec.__index = Vec
local function V(x,y) return setmetatable({x=x,y=y},Vec) end
Vec.__sub = function(a,b) return V(a.x-b.x, a.y-b.y) end
Vec.__mul = function(a,b)
  if type(a)=="number" then return V(a*b.x, a*b.y)
  elseif type(b)=="number" then return V(a.x*b, a.y*b)
  else return a.x*b.x + a.y*b.y  -- dot product
  end
end
Vec.__tostring=function(v) return "("..v.x..","..v.y..")" end

local v = V(3,4)
print(tostring(v * 2))    -- (6,8)
print(tostring(V(1,0) - V(0,1)))  -- (1,-1)

__unm 一元负号

__unm 是一元负号元方法,在 Lua 计算 -obj 时调用。它接收操作数(为保持一致,会传入两次相同的值),并返回取反后的值。

local Vec = {}
Vec.__index = Vec
local function V(x,y) return setmetatable({x=x,y=y},Vec) end
Vec.__unm = function(a) return V(-a.x, -a.y) end
Vec.__tostring = function(v)
  return string.format("(%g,%g)", v.x, v.y)
end

local v = V(3, -4)
print(tostring(-v))    -- (-3,4)
print(tostring(-(-v))) -- (3,-4)

使用 __len 自定义长度

当对没有原始整数序列的表计算 #obj 时,会调用 __len。您可以定义它,为集合、队列或加权容器等自定义类型返回有意义的“长度”。

local Bag = {}
Bag.__index = Bag
Bag.__len = function(b) return b._count end

function Bag.new()
  return setmetatable({_count=0, _items={}}, Bag)
end
function Bag:add(item, qty)
  self._items[item] = (self._items[item] or 0) + (qty or 1)
  self._count = self._count + (qty or 1)
end

local bag = Bag.new()
bag:add("apple", 3)
bag:add("banana", 2)
print(#bag)   -- 5

__mod 和 __pow

__mod 处理 % 运算符;__pow 处理 ^ 运算符。它们不太常用,但对于大整数、分数或模运算对象等自定义数值类型很有用。

local Modular = {}
Modular.__index = Modular
local M = 1000000007

local function mod(n)
  return setmetatable({n = n % M}, Modular)
end

Modular.__add = function(a,b) return mod(a.n + b.n) end
Modular.__mul = function(a,b) return mod(a.n * b.n) end
Modular.__pow = function(a,b)
  local r, base, exp = mod(1), a, b
  while exp > 0 do
    if exp%2==1 then r = r*base end
    base = base*base; exp = math.floor(exp/2)
  end
  return r
end

print((mod(2) ^ 30).n)   -- 73741817 (2^30 mod 1e9+7)

__concat

当至少一个操作数具有此元方法时,__concat 会由 .. 运算符触发。这样可以为自定义类型定义连接操作,或创建流畅的领域专用语言。

local Builder = {}
Builder.__index = Builder
Builder.__concat = function(a, b)
  local result = {}
  for _, v in ipairs(a._parts) do result[#result+1] = v end
  if type(b) == "string" then result[#result+1] = b
  else for _, v in ipairs(b._parts) do result[#result+1] = v end end
  return setmetatable({_parts=result}, Builder)
end
Builder.build = function(b) return table.concat(b._parts) end

local function S(s) return setmetatable({_parts={s}},Builder) end
print((S("Hello") .. ", " .. S("World") .. "!"):build())

比较元方法

__eq 为表定义 ==(仅当两个操作数具有相同元表时才会调用)。__lt 定义 <;__le 定义 <=。注意:~= 使用 __eq;> 使用交换参数后的 __lt;>= 使用 __le。

local Vec = {}
Vec.__index = Vec
local function V(x,y) return setmetatable({x=x,y=y},Vec) end

Vec.__eq = function(a,b) return a.x==b.x and a.y==b.y end
Vec.__lt = function(a,b)
  return (a.x^2+a.y^2) < (b.x^2+b.y^2)  -- by magnitude
end

print(V(1,2) == V(1,2))  -- true
print(V(1,2) == V(1,3))  -- false
print(V(1,0) < V(2,0))   -- true (1 < 2)

链接元方法

由于元方法会返回值(通常是新表),因此可以自然地链接多个操作。每个运算符都会返回一个新对象,下一个运算符则使用该结果。这使得您可以对自定义类型编写流畅的算术表达式。

local Vec = {}
Vec.__index = Vec
local function V(x,y) return setmetatable({x=x,y=y},Vec) end
Vec.__add = function(a,b) return V(a.x+b.x,a.y+b.y) end
Vec.__mul = function(a,b)
  if type(b)=="number" then return V(a.x*b,a.y*b) end
  return V(a.x*b.x+a.y*b.y, 0)   -- simplified
end
Vec.__tostring=function(v) return "("..v.x..","..v.y..")" end

local result = V(1,0) + V(0,1) * 3
print(tostring(result))   -- (1,3)

混合类型运算

当您编写 vec * 2 时,左操作数是表。当您编写 2 * vec 时,左操作数是数字(没有元方法),因此 Lua 会检查右操作数的元表中是否有 __mul。请在元方法中处理这两种顺序。

local Vec = {}
Vec.__index = Vec
local function V(x,y) return setmetatable({x=x,y=y},Vec) end
Vec.__tostring = function(v) return "("..v.x..","..v.y..")" end

Vec.__mul = function(a, b)
  if type(a) == "number" then
    return V(a*b.x, a*b.y)   -- scalar * vec
  elseif type(b) == "number" then
    return V(a.x*b, a.y*b)   -- vec * scalar
  end
  return a.x*b.x + a.y*b.y  -- dot product
end

print(tostring(V(1,2) * 3))  -- (3,6)
print(tostring(3 * V(1,2)))  -- (3,6)

快速检查

-myTable 会触发哪个元方法?

回顾:算术元方法

总结:

  • __add __sub __mul __div __mod __pow __idiv — 二元算术运算
  • __unm — 一元负号;__len — # 运算符
  • __concat — .. 运算符
  • __eq __lt __le — 比较运算符
  • 在元方法内部处理混合类型(数字/表)

常见问题解答

「算术元方法」课时是免费的吗?

是的 — 「算术元方法」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Lua Academy 课程的其余内容,请升级到 CoddyKit PRO。 Lua Academy 课程共包含 4 节课。

「算术元方法」这节课中我会学到什么?

使用元方法重载 +、-、*、/ 及其他运算符。 你通过在浏览器中直接运行的动手代码来练习 Lua Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Lua Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Lua Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「算术元方法」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Lua Academy 课中编写并运行代码吗?

能。每节 Lua Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 元表简介
  2. __index 和 __newindex
  3. 算术元方法
  4. __tostring 和 __len
← 返回 Lua Academy