在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作
。
因此 Lua 提供了元表(Metatable),允许我们改变table的行为
,每个行为关联了对应的元方法。
例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。
当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"__add"的字段,若找到,则调用对应的值。"__add"等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。
有两个很重要的函数来处理元表:
以下实例演示了如何对指定的表设置元表:
t = {}
print(getmetatable(t)) -- nil
可以使用 setmetatable 函数设置或者改变一个表的 metatable
t1 = {}
setmetatable(t,t1)
print(getmetatable(t)) -- table:table: 00A89B30
print(assert(getmetatable(t) == t1)) -- true
任何一个表都可以是其他一个表的 metatable,一组相关的表可以共享一个 metatable
(描述他们共同的行为)
一个表也可以是自身的 metatable(描述其私有行为)。有点像python当中的继承类
简而言之,元表概念:
算术类元方法:
__add(+), __mul(*), __ sub(-), __div(/), __unm, __mod(%), __pow, (__concat)
关系类元方法:
__eq, __lt(<), __le(<=),其他Lua自动转换 a~=b --> not(a == b) a > b --> b < a a >= b --> b <= a (注意NaN的情况)
table访问的元方法:
__index, __newindex
__index
: 查询:访问表中不存的字段& rawget(t, i)
__newindex
: 更新:向表中不存在索引赋值 rawset(t, k, v)
元表本质上来说是一种用来存放元方法的table。我们可以通过对应的key来得到value值,作用就是修改一个值的行为(更确切的说,这是元方法的能力),需要注意的是,这种修改会覆盖掉原本该值可能存在的相应的预定义行为。
lua中的每个值都可以有一个元表,只是table和userdata可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。
lua代码中只能设置table的元表,至于其他类型值的元表只能通过C代码设置。
多个table可以共享一个通用的元表,但是每个table只能拥有一个元表。
我们称元表中的键为事件(event),称值为元方法(metamethod)
。前述例子中的事件是"add",元方法是执行加法的函数。
可通过函数getmetatable查询任何值的元表。
可通过函数setmetatable替换表的元表
lua查找表中的元素时规则如下(非常重要!!需要理解这个
):
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续
3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值
最简单的案例
father = {
house = 1
}
son = {
car = 1
}
setmetatable(son,father)
print(son.house) -- nil
-- father.__index=father
setmetatable(son,{__index=father})
print(son.house) -- 1
尝试用上面查找元素规则理解!!!!
只有使用这个方法才能实现真正意义元表,单存setmetatable并不会找到,所想继承的元表元素
other = { foo = 3}
t = setmetatable({},{__index = other})
print(t.foo) -- 3
print(t.bar) -- nil
如果__index包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数
。
__index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;
如果存在则由 __index 返回结果。
对应函数例子
mytable = setmetatable({ key1 = "value1" }, {
__index = function(mytable, key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
})
print(mytable.key1, mytable.key2,mytable.key3)
-- value1 metatablevalue nil
实例解析:
上面代码可以简写
mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)
当我们访问一个表的不存在的域,返回结果为 nil,这是正确的,但并不一定正确。
实际上,这种访问触发 lua 解释器去查找__index metamethod:如果不存在,
返回结果为 nil;如果存在则由__index metamethod 返回结果
这个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描
述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的值,
当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一
种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认值。第二种方法是,创建一个新的窗口去继承一个原型窗口的缺少域
。首先,我们实现一个原型和一
个构造函数,他们共享一个 metatable:
- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = { x = 0, y = 0, width = 100, height = 100, }
-- create a metatable
Window.mt = {}
-- declare the constructor function
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
现在我们定义__index metamethod
Window.mt.__index = function(table, key)
return Window.prototype[key]
end
这样一来,我们创建一个新的窗口,然后访问他缺少的域结果如下:
w = Window.new{x=10, y=20}
print(w.width) --> 100
Lua 发现 w 不存在域 width 时,但是有一个 metatable 带有__index 域,Lua 使用
w(the table)和 width(缺少的值)来调用__index metamethod,metamethod 则通过访问
原型表(prototype)获取缺少的域的结果。
__index metamethod 在继承中的使用非常常见,所以 Lua 提供了一个更简洁的使用
方式。__index metamethod 不需要非是一个函数,他也可以是一个表。但它是一个函数
的时候,Lua 将 table 和缺少的域作为参数调用这个函数;当他是一个表的时候,Lua 将
在这个表中看是否有缺少的域。所以,上面的那个例子可以使用第二种方式简单的改写
为:
Window.mt.__index = Window.prototype
现在,当 Lua 查找 metatable 的__index 域时,他发现 window.prototype 的值,它是
一个表,所以 Lua 将访问这个表来获取缺少的值,也就是说它相当于执行:
Window.prototype["width"]
将一个表作为__index metamethod 使用,提供了一种廉价而简单的实现单继承的方
法。一个函数的代价虽然稍微高点,但提供了更多的灵活性:我们可以实现多继承,隐
藏,和其他一些变异的机制。我们将在第 16 章详细的讨论继承的方式。
当我们想不通过调用__index metamethod 来访问一个表,我们可以使用 rawget 函数。
Rawget(t,i)的调用以 raw access 方式访问表。这种访问方式不会使你的代码变快(the
overhead of a function call kills any gain you could have),但有些时候我们需要他,在后面
我们将会看到。
将上述方法精简
Window = {}
Window.prototype = {
x = 0,
y = 0,
width = 100,
height = 100
}
Window.mt = {}
w = setmetatable({x=10,y=10},{__index=Window})
print(w.width) -- nil Window表中并没有width键
print(w.prototype.width) -- 100
--- 将继承元表深入
w = setmetatable({x=10,y=10},{__index=Window.prototype})
print(w.width) -- 100
print(w.x) -- 10
__newindex 元方法用来对表更新,__index则用来对表访问 。
mymetatable = {}
mytable = setmetatable({key1 = 'value1'},
{__newindex=mymetatable})
print(mytable.key1) -- value1
-- 第一部分
mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey) -- nil 新值2
-- 第二部分
mytable.key1 = "新值1"
print(mytable.key1,mymetatable.newkey1) -- 新值1 nil
第一部分理解:
当对一个表进行键值操作的时候,先在原表mytable进行查找,如果没有,则判断有无元表,有元表则进行元方法查找,发现有__newindex,对应到mymetatable,对mymetatable进行查找,发现没有,查找是否有继承元表,发现也没有,则在mymetatable进行新增一个newky = ‘新增2’
第二部分理解:
当对一个表进行键值操作的时候,先在原表mytable进行查找,发现有对应的键,那么对此表进行更新值
mytable = setmetatable({key1 = "value1"}, {
__newindex = function(mytable, key, value)
rawset(mytable, key, "\""..value.."\"")
end
})
mytable.key1 = "new value"
mytable.key2 = 4
print(mytable.key1,mytable.key2) -- new value "4"
function setDefault(t,d)
local mt = {__index = function () return d end}
setmetatable(t,mt)
end
tab = {x=10,y=10}
print(tab.x,tab.y,tab.z)
setDefault(tab,0)
print(tab.x,tab.y,tab.z)
print(tab.c)
10 10 nil
10 10 0
0
这种创建,不管什么时候我们访问表的缺少域,他的元方法都会返回默认值 0
案1方法会消耗大量的metatable,如果只使用一个metatable,会大大地优化,但是不同的table有-- 不同的默认值,这种方法将默认值存储在各个table的自己域中
为每一个表创建自己的域,存储自己的默认值
。
为了解决这种方法,使用一个唯一的域存储在表本身里面,如果不担心命名的混乱,可以使用" ___ "
(三个下划线) 作为唯一域
表示方式
local mt = {__index = function (t) return t.___ end}
function setDefault (t, d)
t.___ = d
setmetatable(t, mt)
end
tab1 = {x=10}
setDefault(tab1,1)
print(tab1.x,tab1.z,tab1.y)
tab2 = {x=25}
setDefault(tab2,2)
print(tab2.___,tab2.__)
如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创
建一个新表用作键值:(因为涉及到table中域的唯一性问题,当然"__"域在实际使用中也算唯一,可以另外用一个key保证域的唯一性)
local key = {} -- unique key
local mt = { __index = function(t)
return t[key]
end }
function setDefault (t, d)
t[key] = d -- 保证域的唯一性
setmetatable(t, mt)
end
tab1 = {x=10}
setDefault(tab1,3)
print(tab1.x,tab1.s)
保证统一性,将各个table以及他们的默认值保存在一个公共的table中,不过这个table需要是weak table
– 如果这个公共table是个普通表的话,那么作为key的table就会假设永远存在,不会被Lua回收
– 我们这个weak table的key需要是weak,这样作为key的table如果没有被引用,会被Lua回收
defaults = {}
setmetatable(defaults, { __mode = "k" }) --key是weak的weak table
mt = { __index = function(t)
return defaults[t]
end }
function setDefault (t, d)
defaults[t] = d
setmetatable(t, mt)
end
上面的方法已经很好了,但是假设有成千上万个table,但是table的默认值就几种,那么那个公共的defaults表
– 中保存的信息就很冗余了
– 这种方法就是通过默认值来保存元表,相同的默认值使用相同的元表,不同的默认值使用不同的元表?
– 自然这种方法使用了记忆技术(memoize)
– 用一个公共的table去保存元表,key是默认值,value是默认值对应的元表
– 当然没有人再使用metatable的时候要允许lua的GC去回收它,因此这个公共的table应该是value是weak的weak table
metas = {}
setmetatable(metas, {__mode = "v"})
function setDefault (t,d)
local res = metas[d]
if res == nil then
res = {__index = function () return d end}
metas[d] = res -- memoize 这就是记忆技术啦~~
end
setmetatable(t, res)
end
__index和 __newindex 都是只有当表中访问的域不存在时候才起作用。
--原始表
t = {}
-- 保持对原始表的私有访问
local _t = t
-- 创建代理
t = {}
-- 创建元表
local mt = {
__index = function(t, k)
print("访问元素 " .. tostring(k))
return _t[k] -- 访问原始表
end,
__newindex = function(t, k, v)
print("更新元素 " .. tostring(k) ..
" to " .. tostring(v))
_t[k] = v -- 更新原始表
end
}
setmetatable(t, mt)
t[2] = 'hello'
print(t[2])
更新元素 2 to hello
访问元素 2
hello
(注意:不幸的是,这个设计不允许我们遍历表。Pairs 函数将对 proxy 进行操作,而不是原始的表)
将上面的思想汇总,最终的结果如下:
local index = {} -- 创建私人索引
local mt = {
__index = function(t, k)
print("*access to element " .. tostring(k))
return t[index][k] -- 查询原始表
end,
__newindex = function(t, k, v)
print("*update of element " .. tostring(k) .. " to "
.. tostring(v))
t[index][k] = v -- 更新原始表
end
}
function track (t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
tab1 = {x=10}
tab1 = track(tab1)
tab1[2] = 'hello'
创建一个总代理proxy,代理分配一个私有域 index = {} ,存放着不同table,并且此代理继承元表
*update of element 2 to hello
function readOnly (t)
local proxy = {}
local mt = {
__index = t,
__newindex = function(t, k, v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
days = readOnly { "Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday" }
print(days[1]) --> Sunday
days[2] = "Noday"
meta2 = {
-- 当子表要被当做字符串使用时,会默认调用这个元表中的tostring方法
__tostring = function()
return '老王'
end
}
myTable2 = {}
setmetatable(myTable2,meta2)
print(myTable2) -- 老王
tostring加个参数,默认会将子表传入进去,调用子表属性
meta2 = {
-- 当子表要被当做字符串使用时,会默认调用这个元表中的tostring方法
__tostring = function(t)
return t.name
end
}
myTable2 = {
name = '老王'
}
setmetatable(myTable2,meta2)
print(myTable2) -- 老王
当子表被当做一个函数来使用时,会默认调用这个__call中的内容
meta3 = {
-- 当子表要被当做字符串使用时,会默认调用这个元表中的tostring方法
__tostring = function(t)
return t.name
end,
-- 当子表被当做一个函数来使用时,会默认调用这个__call中的内容
__call = function(a)
print(a)
print('老王在骑车')
end
}
myTable3 = {
name = '老王'
}
setmetatable(myTable3,meta3)
--print(myTable3)
myTable3(1)
我传入了子表 1 参数,但是打印的并不是1,而是自己,相当于 __call方法,如果传入参数的时候,是将自己子表作为参数传入,受tostring的影响,将本身传入,如果注释tostring,那么打印的是table
首先,tostring如果在的时候,那么,已经传入了子表,但是注释了tostring,
那么,到call的时候,第一个参数为子表,第二个参数为1
meta4 = {}
myTable4 = {}
setmetatable(myTable4,meta4)
myTable5 = {}
print(myTable4 + myTable5) -- 报错
那么将meta4改造一下
meta4 = {
-- 相当于运算符重载,当子表使用+运算符时,会调用该方法
__add = function(t1,t2)
return t1.age + t2.age
end
}
myTable4 = {age=1}
setmetatable(myTable4,meta4)
myTable5 = {age=2}
print(myTable4 + myTable5) -- 3
其它运算符
meta4 = {
-- 相当于运算符重载,当子表使用+运算符时,会调用该方法
__add = function(t1,t2)
return t1.age + t2.age
end,
__sub = function(t1,t2)
return t1.age - t2.age
end,
__mul = function(t1,t2)
return t1.age * t2.age
end,
__div = function(t1,t2)
return t1.age / t2.age
end,
__mod = function(t1,t2)
return t1.age % t2.age
end,
__pow = function(t1,t2)
return t1.age ^ t2.age
end,
__eq = function(t1,t2)
return t1.age == t2.age
end,
__lt = function(t1,t2)
return t1.age < t2.age
end,
__le = function(t1,t2)
return t1.age > t2.age
end,
__concat = function(t1,t2)
return t1.age .. t2.age
end,
}
myTable4 = {age=1}
setmetatable(myTable4,meta4)
myTable5 = {age=2}
print(myTable4 + myTable5) -- 3
__index 最好不要写在元表里面,写在外部
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续
3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值
meta6 = {
age =1
}
meTable6 ={}
setmetatable(meTable6 ,{__index = meta6})
print(meTable6.age)
__newindex
当复制时,如果复制一个不存在的索引
那么会把这个值赋值到newinex所指的表中,不会修改自己
meta7 = {}
meta7.__newindex = {}
myTable7 = {}
setmetatable(myTable7,meta7)
meta7.age =1
print(myTable7.age)
print(meta7.age)