metatable是Lua中的重要概念,每一个table都可以加上metatable,以改变相应的table的行为。让我们看一个例子:
t = {} -- 普通的table
mt = {} -- metatable
setmetatable(t,mt) -- 设定mt为t的metatable
getmetatable(t) -- 返回mt
使用getmetatable
和setmetatable
来查看和设定metatable。当然,上面的代码也可以压缩成一行:
t = setmetatable({},{})
这是因为setmetatable
会返回它的第一个参数。
metatable可以包括任何东西,metatable特有的键一般以__
开头,例如__index
和__newindex
,它们的值一般是函数或其他table。
t = setmetatable({},{
__index = function(t,key)
if key == "foo" then
return 0
else
return table[key]
end
end
})
__index
这是metatable最常用的键了。
当你通过键来访问table的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index
键。如果__index
包含一个表格,Lua会在表格中查找相应的键。
other = { foo = 3 }
t = setmetatable({},{ __index = other })
t.foo -- 3
t.bar -- nil
如果__index
包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。
__newindex
类似__index
,__newindex
的值为函数或table,用于按键赋值的情况。
other = {}
t = setmetatable({},{ __newindex = other })
t.foo = 3
other.foo -- 3
t.foo -- nil
t = setmetatable({},{
__newindex = function(t,key,value)
if type(value) == "number" then
rawset(t,value * value)
else
rawset(t,value)
end
end
})
t.foo = "foo"
t.bar = 4
t.la = 10
t.foo -- "foo"
t.bar -- 16
t.la -- 100
上面的代码中使用了rawget
和rawset
以避免死循环。使用这两个函数,可以避免Lua使用__index
和__newindex
。
运算符
利用metatable可以定义运算符,例如*
:
t = setmetatable({ 1,2,3 },{
__mul = function(t,other)
new = {}
for i = 1,other do
for _,v in ipairs(t) do table.insert(new,v) end
end
return new
end
})
t = t * 2 -- { 1,3,1,3 }
和__index
、__newindex
不同,__mul
的值只能是函数。与__mul
类似的键有:
-
__add
(+)
-
__sub
(-)
-
__div
(/)
-
__mod
(%)
-
__unm
取负
-
__concat
(..)
-
__eq
(==)
-
__lt
(<
)
-
__le
(<=
)
__call
__call
使得你可以像调用函数一样调用table:
t = setmetatable({},{
__call = function(t,a,b,c,whatever)
return (a + b + c) * whatever
end
})
t(1,4) -- 24
这是很有用的特性。需要以直接调用table的形式调用table中的某个(默认)函数的时候,使用__call
设定很方便。例如,kikito的tween.lua,就用了这个技巧,这样直接调用tween
就可以调用tween.start
。再如MiddleClass中,类的new
方法可以通过直接调用类的方式调用。
__tostring
最后讲下__tostring
,它可以定义如何将一个table转换成字符串,经常和print
配合使用,因为默认情况下,你打印table的时候会显示table: 0x<16进制数字>
:
t = setmetatable({ 1,{
__tostring = function(t)
sum = 0
for _,v in pairs(t) do sum = sum + v end
return "Sum: " .. sum
end
})
print(t) -- prints out "Sum: 6"
例子
综合运用以上知识,我们编写一个2D矢量类:
Vector = {}
Vector.__index = Vector
function Vector.__add(a,b)
if type(a) == "number" then
return Vector.new(b.x + a,b.y + a)
elseif type(b) == "number" then
return Vector.new(a.x + b,a.y + b)
else
return Vector.new(a.x + b.x,a.y + b.y)
end
end
function Vector.__sub(a,b)
if type(a) == "number" then
return Vector.new(b.x - a,b.y - a)
elseif type(b) == "number" then
return Vector.new(a.x - b,a.y - b)
else
return Vector.new(a.x - b.x,a.y - b.y)
end
end
function Vector.__mul(a,b)
if type(a) == "number" then
return Vector.new(b.x * a,b.y * a)
elseif type(b) == "number" then
return Vector.new(a.x * b,a.y * b)
else
return Vector.new(a.x * b.x,a.y * b.y)
end
end
function Vector.__div(a,b)
if type(a) == "number" then
return Vector.new(b.x / a,b.y / a)
elseif type(b) == "number" then
return Vector.new(a.x / b,a.y / b)
else
return Vector.new(a.x / b.x,a.y / b.y)
end
end
function Vector.__eq(a,b)
return a.x == b.x and a.y == b.y
end
function Vector.__lt(a,b)
return a.x < b.x or (a.x == b.x and a.y < b.y)
end
function Vector.__le(a,b)
return a.x <= b.x and a.y <= b.y
end
function Vector.__tostring(a)
return "(" .. a.x .. "," .. a.y .. ")"
end
function Vector.new(x,y)
return setmetatable({ x = x or 0,y = y or 0 },Vector)
end
function Vector.distance(a,b)
return (b - a):len()
end
function Vector:clone()
return Vector.new(self.x,self.y)
end
function Vector:unpack()
return self.x,self.y
end
function Vector:len()
return math.sqrt(self.x * self.x + self.y * self.y)
end
function Vector:lenSq()
return self.x * self.x + self.y * self.y
end
function Vector:normalize()
local len = self:len()
self.x = self.x / len
self.y = self.y / len
return self
end
function Vector:normalized()
return self / self:len()
end
function Vector:rotate(phi)
local c = math.cos(phi)
local s = math.sin(phi)
self.x = c * self.x - s * self.y
self.y = s * self.x + c * self.y
return self
end
function Vector:rotated(phi)
return self:clone():rotate(phi)
end
function Vector:perpendicular()
return Vector.new(-self.y,self.x)
end
function Vector:projectOn(other)
return (self * other) * other / other:lenSq()
end
function Vector:cross(other)
return self.x * other.y - self.y * other.x
end
setmetatable(Vector,{ __call = function(_,...) return Vector.new(...) end })
原文 Lua Metatables Tutorial
翻译 SegmentFault