Lua中每个值都有一个元表metatable
,这个元表metatable
是一个原始的Lua表table
,元表metatable
用来定义原始值在特定操作下的行为。通过在元表metatable
中的特定域设一些值来改变拥有这个元表metatable
的值的指定操作的行为。我们称元表metatable
中的键名为事件event
,将其中的值叫做元方法metamethod
。
Lua5.1中元表metatable
的表现行为类似于C++语言中的操作符重载,例如:重载__add
元方法metamethod
,来计算两个Lua数组的并集。重载__index
元方法来定义自己的Hash
函数。
Lua提供两个用来处理元表的方法
-
setmetatable(table, metatable)
为表设置元表metatable
,不能从Lua中改变其它任何类型的值的元表metatable
(使用debug
库例外),要这样做的话必须使用C语言的API。 -
getmetatable(table)
获取表的元表metatable
对象
设置元表
local tbl = {}
local metatbl = {}
local mytbl = setmetatable( tbl, metatbl )
元表(metatable)与 元方法(metamethod)
通常,Lua中每个值都有一套预定义的操作集合。例如,数字相加、连接字符串、表插入key-value
对等,但无法将两个表相加,无法对函数比较等。此时,可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。
- Lua中每个值都有一个元表
metatable
table
和userdata
可以有各自独立的元表metatable
,其他类型的值共享其类型所属的单一元表。 - Lua中只能设置表
table
的元表metatable
,设置其他类型值得元表必须通过C代码实现。 - Lua在创建新的表
table
时不会创建元表metatable
表与元表
- 表
table
可作为任何值得元表metatable
,一组相关的表可以共享一个通用的元表,通用的元表描述了它们共同的行为。 - 表
table
可作为自己的元表metatable
,用于描述其特有的行为。
表的元表获取与修改
- 使用
setmettable
来设置或修改表table
的元表metatable
- 使用
getmettable
来获取元表
Lua提供的所有操作符都可以被重载
-
__add
加法操作 -
__sub
减法操作 -
__mul
乘法操作 -
__div
除法操作 -
__mod
求模取余操作 -
__pow
乘幂操作 -
__unm
一元负数操作 -
__concat
字符串连接操作 -
__len
长度获取 -
__eq
相等比较,getcomphandler
函数定义了Lua怎样选择一个处理器来做比较操作,仅在两个对象类型相同且有对应操作相同的元方法时才起效。 -
__lt
小于比较 -
__le
小于等于比较
除了操作符之外,如下元方法也可以被重载。
-
__index
取下标操作用于访问table[key]
-
__newindex
赋值给指定下标table[key]=value
-
__tostring
转换成字符串 -
__call
当Lua调用一个值时调用 -
__mode
用于弱表week table
-
__metatable
用于保护metatable
不被访问
算术运算
local x = 10
local y = 10
print(x+y) -- 20
表的加法运算
当Lua试图将两个表相加时,会首先检查两个表之一是否有元素,然后检查该元表中是否具有一个叫做__add
的字段。如果Lua找到了该字段,则会调用该字段对应的值。这个值就是所谓的“元方法”。
local tbl1 = {1,2,3}
local tbl2 = {3,2,1}
-- print(#tbl1, #tbl2)
-- 无法直接相加两个表
-- printf(tbl1 + tbl2)
-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
local result = {}
local length = #x
for i=1,length do
result[i] = x[i] + y[i]
end
return result
end
-- test
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的相加操作
local result = tbl1 + tbl2
-- 循环输出
for k,v in ipairs(result) do
print(k, v)
end
表的连接
-- 表的连接
local tbl1 = {1,2,3}
local tbl2 = {3,2,1}
-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
local result = {}
local length = #x
for i=1,length do
result[i] = x[i] + y[i]
end
return result
end
-- 实现表的连接操作
mt.__concat = function(x, y)
local length = #x
local result = {}
for i=1,length do
result[i] = x[i]..y[i]
end
return result
end
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的连接操作
local result = tbl1..tbl2
-- 循环输出
for k,v in ipairs(result) do
print(k, v)
end
--[[
1 13
2 22
3 31
--]]
集合运算
使用表table
表示集合,实现集合计算中的并集、交集等。
-- 定义集合
Set = {}
-- 定义集合的元表
local mt = {}
-- 创建集合,根据参数列表值创建新集合
function Set.new(arr)
local set = {}
setmetatable(set, mt) --创建集合均具有一个相同的元表
for i,v in ipairs(arr) do
set[v] = true
end
return set
end
-- 求集合并集
function Set.union(x, y)
local result = Set.new{}
for k,v in pairs(x) do
result[k] = true
end
for k,v in pairs(y) do
result[k] = true
end
return result
end
-- 求集合交集
function Set.intersection(x, y)
-- 创建空集合
local result = Set.new{}
for k in pairs(x) do
result[k] = y[k]
end
return result
end
-- 将集合转换为字符串
function Set.tostring(set)
-- 定义存放集合中所有元素的列表
local result = {}
for k in pairs(set) do
result[#result + 1] = k
end
return "{"..table.concat(result, ", ").."}"
end
-- 打印输出集合元素
function Set.print(set)
print(Set.tostring(set))
end
-- 设置集合元方法
mt.__add = Set.union
-- 测试
local set1 = Set.new{10,20,30}
local set2 = Set.new{30, 1}
-- 判断集合是否具有相同元表
-- print(getmetatable(set1), getmetatable(set2))
-- table: 0000000000bb9af0 table: 0000000000bb9af0
Set.print(set1 + set2) -- {20, 1, 10, 30}
Set.print(Set.intersection(set1, set2)) -- {30}
__index
当访问表中不存在字段时会得到nil
的结果,实际上访问表时解释器会去查找__index
元方法。如果没有__index
原方法则返回nil
,否则输出结果。
实现在表中查找键不存在时转而在元表中查找该键
-- 原始表
local oriTable = {key = "value"}
-- 元表
local metaTable = {
-- 重载函数
__index = function(self, key)
if key=="key" then
return "metatable value"
end
end
}
local mytbl = setmetatable( oriTable, metaTable )
print(mytbl.key, mytbl.key2)-- value nil
场景:继承
示例:创建描述窗口的表,每个表具有窗口的必备属性和默认值,例如大小、颜色、位置等。我们希望创建窗口对象时,仅指定不同于默认值的参数。
实现方式1:使用构造函数,输入不存在的字段。
实现方式2:新窗口从原型窗口处继承不存在的字段
-- 声明原型
Window = {} -- 创建命名空间
-- 使用默认值创建原型
Window.prototype = {x=0, y=0, width=0, height=0}
-- 创建原型的元表
Window.mt = {}
-- 定义访问表的方法
-- 当Lua检测到当前对象中不存在某字段,但在其metatable中却存在__index字段时,Lua会以指定函数来调用__index。
Window.mt.__index = function(table, key)
return Window.prototype[key]
end
-- 声明原型的构造函数
function Window.new(tbl)
setmetatable(tbl, Window.mt)
return tbl
end
-- 测试:创建新窗口并查询字段
local window = Window.new{x=100, y=100}
print(window.x, window.y, window.width, window.height) -- 100 100 0 0
--判断表中是否存在元素
print(window.x) -- 100
-- 获取元表的值
print(rawget(window, x)) -- nil
Lua中,将__index
用于继承是很普遍的的方式,Lua还提供一种更为便捷的方式实现继承。__index
不必一定是一个函数,可以是一个table
。
- 当是函数时,Lua以
table
和不存在的key
作为参数来调用该函数。 - 当是表时,Lua以相同的方式重新访问这个表。
-- 声明原型
Window = {} -- 创建命名空间
-- 使用默认值创建原型
Window.prototype = {x=0, y=0, width=0, height=0}
-- 创建原型的元表
Window.mt = {}
-- 定义访问表的方法
-- 当Lua查找metatable的__index字段时,若__index字段值是一个table,则会在原型中继续查找,也就是说Lua会在table中重复这个访问过程。
Window.mt.__index = Window.prototype
-- 声明原型的构造函数
function Window.new(tbl)
setmetatable(tbl, Window.mt)
return tbl
end
-- 测试:创建新窗口并查询字段
local window = Window.new{x=100, y=100}
print(window.x, window.y, window.width, window.height) -- 100 100 0 0
--判断表中是否存在元素
print(window.x) -- 100
将table
作为__index
是一种快捷的实现单一继承的方式,虽然将函数作为__index
来实现的方式会带来性能上的较大开销,但函数更加灵活,可通过函数来实现多继承、缓存等功能。
Window.mt.__index = Window.prototype
Window.mt.__index = function(table, key)
return Window.prototype[key]
end
如果不想在访问table
时涉及__index
,可使用rawget(table, key)
进行原始的访问,也就是不考虑metatable
的简单访问,一次原始访问并不会加速代码执行,但有时会使用到。
-- 获取原始值即元表中的值
print(rawget(window, x)) -- nil
设置表字段的默认值
local tbl = {x=1, y=2}
-- table中字段默认值为nil
print(tbl.x, tbl.y, tbl.z) -- 1 2 nil
-- 通过metatable修改table的默认值
function setTableDefault(tbl, default)
local mt = {
__index = function()
return default
end
}
setmetatable(tbl, mt)
end
-- 调用setTableDefault后,任何对tbl中存在的字段的访问都回调用它的__index
setTableDefault(tbl, 0)
print(tbl.x, tbl.y, tbl.z) -- 1 2 0
__newindex
__newindex
与__index
类似,不同之处在于__newindex
用于table
的更新,__index
用于table
的查询。
对table
中不存在的索引赋值时,解释器会查找__nexindex
,若存在则直接调用而不执行赋值。若__newindex
是一个table
,解释器会在此table
中执行赋值,而非原始的table
。
-- 定义原表
local mt = {}
mt.__index = function(tbl, key)
return mt[key]
end
mt.__newindex = function(tbl, key, value)
mt[key] = value
print(string.format("modify: key=%s value=%s", key, val))
end
local window = {x=1}
setmetatable(window, mt)
print(window.x) -- 1
print(rawget(window, x)) -- nil
-- 添加属性
window.y = 2
print(window.y) -- 2
print(rawget(window, y)) -- nil
可通过调用rawset(table, key, value)
绕过__newindex()
,即可不涉及任何原方法而直接设置table
中的键值对。
__tostring
-- 定义原表
local mt = {}
mt.__tostring = function(tbl)
local result = {}
for k,v in pairs(tbl) do
result[#result+1] = k.."="..v
end
return "{"..table.concat(result,",").."}"
end
local window = {x=1}
setmetatable(window, mt)
print(window.x) -- 1
print(rawget(window, x)) -- nil
-- 添加属性
window.y = 2
print(window.y) -- 2
print(rawget(window, y)) -- nil
-- 打印
-- print(window) -- tab-- le: 0000000000b994f0
for k,v in pairs(window) do
print(k, v)
end
print(window) -- {x=1,y=2}