讲到元表,先看一段table的合并动作.
t1 = {1,2}
t2 = {3,4}
t3 = t1 + t2
attempt to perform arithmetic on a table value (global 't1')
程序会报错,因为不知道如何对两个table执行+运算,这个时候就需要通过元表来定义,有点类似c中的运算符加载。我们看一下如何通过元表实现合并操作。
local mt = {}
--定义mt.__add元方法(其实就是元表中一个特殊的索引值)为将两个表的元素合并后返回一个新表
mt.__add = function(t1,t2)
local temp = {}
for _,v in pairs(t1) do
table.insert(temp,v)
end
for _,v in pairs(t2) do
table.insert(temp,v)
end
return temp
end
local t1 = {1,2,3}
local t2 = {2}
--设置t1的元表为mt
setmetatable(t1,mt)
local t3 = t1 + t2
--输出t3
local st = "{"
for _,v in pairs(t3) do
st = st..v..", "
end
st = st.."}"
print(st)
{1, 2, 3, 2, }
可以看到, 程序在执行的时候,调用了mt._add元方法计算。
具体的过程是:
1.查看t1是否有元表,若有,则查看t1的元表是否有__add元方法,若有则调用。
2.查看t2是否有元表,若有,则查看t2的元表是否有__add元方法,若有则调用。
3.若都没有则会报错。
所以说,我们通过定义了t1元表的__add元方法,达到了让两个表通过+号来相加的效果
Lua 查找一个表元素时的规则,其实就是如下 3 个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
3.判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。
Lua提供两个用来处理元表的方法
-
setmetatable(table, metatable)
为表设置元表metatable
,不能从Lua中改变其它任何类型的值的元表metatable
(使用debug
库例外),要这样做的话必须使用C语言的API。 -
getmetatable(table)
获取表的元表metatable
对象
元表的元方法有:(下标是__双底线喔)
函数 | 描述 |
---|---|
__add | 运算符 + |
__sub | 运算符 - |
__mul | 运算符 * |
__ div | 运算符 / |
__mod | 运算符 % |
__unm | 运算符 -(取反) |
__concat | 运算符 .. |
__eq | 运算符 == |
__lt | 运算符 < |
__le | 运算符 <= |
__call | 当函数调用 |
__tostring | 转化为字符串 |
__index | 调用一个索引 |
__newindex | 给一个索引赋值 |
__index
取下标操作用于访问`table[key]
__newindex
赋值给指定下标`table[key]=value
__tostring
转换成字符串
__call
当Lua调用一个值时调用
__mode
用于弱表`week table
__metatable
用于保护metatable
不被访问
- __add
当Lua试图将两个表相加时,会首先检查两个表之一是否有元素,然后检查该元表中是否具有一个叫做__add
的字段。如果Lua找到了该字段,则会调用该字段对应的值。这个值就是所谓的“元方法”。
local tbl1 = {1,2,3}
local tbl2 = {5,1,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
1 6
2 3
3 4
- __concat 表连接
-- 表的连接
local tbl1 = {1,2,3}
local tbl2 = {2,3,4}
-- 实现表的相加操作
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 1**2
2 2**3
3 3**4
下面举一个例子,集合运算
使用表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,40}
local set2 = Set.new{30, 1,50}
Set.print(set1 + set2) -- {1, 40, 10, 20, 30, 50}
Set.print(Set.intersection(set1, set2)) -- {30}
- __call
__call可以让table当做一个函数来使用。
local mt = {}
--__call的第一参数是表自己
mt.__call = function(mytable,...)
--输出所有参数
for _,v in ipairs{...} do
print(v)
end
end
t = {}
setmetatable(t,mt)
--将t当作一个函数调用
t(1,2,3)
1
2
3
再举一个例子,注意call 里面的参数调用
local mt = {}
sum = 0
--__call的第一参数是表自己
mt.__call = function(mytable,val)
--输出所有参数
for i = 1,#mytable do
sum = sum +mytable[i]*val
end
return sum
end
t = {1,2,3}
setmetatable(t,mt)
--将t当作一个函数调用
print(t(5))
--30
- __tostring
__tostring可以修改table转化为字符串的行为
local mt = {}
--参数是表自己
mt.__tostring = function(t)
local s = "{"
for i,v in ipairs(t) do
if i > 1 then
s = s..", "
end
s = s..v
end
s = s .."}"
return s
end
t = {1,2,3}
--直接输出t
print(t)
--将t的元表设为mt
setmetatable(t,mt)
--输出t
print(t)
table: 0x7fcfe7c06a80
{1, 2, 3}
- __index
调用table的一个不存在的索引时,会使用到元表的__index元方法,和前几个元方法不同,__index可以是一个函数也可是一个table。
作为函数:
将表和索引作为参数传入__index元方法,return一个返回值
local mt = {}
--第一个参数是表自己,第二个参数是调用的索引
mt.__index = function(t,key)
return "it is "..key
end
t = {1,2,3}
--输出未定义的key索引,输出为nil
print(t.key)
setmetatable(t,mt)
--设置元表后输出未定义的key索引,调用元表的__index函数,返回"it is key"输出
print(t.key)
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, 1)
print(tbl.x, tbl.y, tbl.z) -- 1 2 1
作为table:
查找__index元方法表,若有该索引,则返回该索引对应的值,否则返回nil
local mt = {}
mt.__index = {key = "it is key"}
t = {1,2,3}
--输出未定义的key索引,输出为nil
print(t.key)
setmetatable(t,mt)
--输出表中未定义,但元表的__index中定义的key索引时,输出__index中的key索引值"it is key"
print(t.key)
--输出表中未定义,但元表的__index中也未定义的值时,输出为nil
print(t.key2)
nil
it is key
nil
- __newindex
__newindex
与__index
类似,不同之处在于__newindex
用于table
的更新,__index
用于table
的查询;当为table中一个不存在的索引赋值时,会去调用元表中的__newindex元方法
1.作为函数
__newindex是一个函数时会将赋值语句中的表、索引、赋的值当作参数去调用。不对表进行改变
local mt = {}
--第一个参数时表自己,第二个参数是索引,第三个参数是赋的值
mt.__newindex = function(t,index,value)
print("index is "..index)
print("value is "..value)
end
t = {key = "it is key"}
setmetatable(t,mt)
--输出表中已有索引key的值
print(t.key)
--为表中不存在的newKey索引赋值,调用了元表的__newIndex元方法,输出了参数信息
t.newKey = 10
--表中的newKey索引值还是空,上面看着是一个赋值操作,其实只是调用了__newIndex元方法,并没有对t中的元素进行改动
print(t.newKey)
it is key
index is newKey
value is 10
nil
-- 定义原表
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, value))
end
local window = {x=1}
setmetatable(window, mt)
print(window.x) -- 1
print(rawget(window, x)) -- nil
-- 添加属性
print ("-------------")
window.y = 2
print ("-------------")
for k,v in pairs(mt) do
print (k,v)
end
print ("-------------")
for k in pairs(mt) do
print (k)
end
print ("-------------")
print(window.y) -- 2
print(rawget(window, y)) -- nil
1
nil
-------------
modify: key=y value=2
-------------
__index function: 0x7fde254066f0
y 2
__newindex function: 0x7fde25406b00
-------------
__index
y
__newindex
-------------
2
nil
2.作为table
__newindex是一个table时,为t中不存在的索引赋值会将该索引和值赋到__newindex所指向的表中,不对原来的表进行改变。
local mt = {}
--将__newindex元方法设置为一个空表newTable
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)
--对t中不存在的索引进行负值时,由于t的元表中的__newindex元方法指向了一个表,所以并没有对t中的索引进行赋值操作将,而是将__newindex所指向的newTable的newKey索引赋值为了"it is newKey"
t.newKey = "it is newKey"
print(t.newKey,newTable.newKey)
nil nil
nil it is newKey
当然如果主表中存在该索引,自然会直接赋值,不会传递元表中赋值。我们也可以直接改写newindex,用rawset直接赋值
Window = {}
Window.mt = {}
function Window.new(o)
setmetatable(o ,Window.mt)
return o
end
Window.mt.__index = function (t ,key)
return 1000
end
Window.mt.__newindex = function (table ,key ,value)
if key == "wangbin" then
rawset(table ,"wangbin" ,"yes,i am")
end
end
w = Window.new{x = 10 ,y = 20}
w.wangbin = "55"
print(w.wangbin)
yes,i am
rawget 和 rawset
有时候我们希望直接改动或获取表中的值时,就需要rawget和rawset方法了。
rawget可以让你直接获取到表中索引的实际值,而不通过元表的__index元方法。
rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效
local mt = {}
mt.__index = {key = "it is key"}
t = {}
setmetatable(t,mt)
print(t.key)
--通过rawget直接获取t中的key索引
print(rawget(t,"key"))
it is key
nil
rawset可以让你直接为表中索引的赋值,而不通过元表的__newindex元方法。
local mt = {}
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)
--通过rawset直接向t的newKey索引赋值
rawset(t,"newKey","it is newKey")
print(t.newKey,newTable.newKey)
nil nil
it is newKey nil
local mt = {}
t = {}
setmetatable(t,mt)
rawset(t,"newKey","it is newKey")
for k ,v in pairs (t) do
print (k,v)
end
print(t.newKey)
newKey it is newKey
it is newKey
下面举几个例子,讲述一下各个方法之间的关系。
local tb = {}
setmetatable(tb, { __index = function()
return "not find"
end })
setmetatable(tb, { __newindex = function(table, key, value)
local patchKey = "version"
if key == patchKey then
rawset(table, patchKey, "补丁值")
else
rawset(table, key, value)
end
end })
-- setmetatable(tb, { __index = function()
-- return "not find"
-- end })
tb.version = "正常版本"
tb.date = "2018"
print(tb.version) --打印 补丁值
print(tb.server) --打印nil,不会调用__index方法了?
print(tb.date) --打印2018
经过测试发现:
如果__index在__newindex之前,则不会调用__index
如果把_index放在__newindex之后,调用不存在值,才会调用__index方法
--谁在后面就会调用谁,前一个会失效。但是这个取决于你定于元方法的方式(我们一般定义元方法方式如下),看下面的写法没问题;
local tb = {}
local mt ={}
mt.__newindex = function(table, key, value)
local patchKey = "version"
if key == patchKey then
rawset(table, patchKey, "补丁值")
else
rawset(table, key, value)
end
end
mt.__index = function()
return "not find"
end
setmetatable(tb,mt)
tb.version = "正常版本"
tb.date = "2018"
print(tb.version)
print(tb.server)
print(tb.date)
补丁值
not find
2018
rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效
--- Gets the real value of `table[index]`, the `__index` metamethod. `table`
--- must be a table; `index` may be any value.
---@param table table
---@param index any
---@return any
function rawget(table, index) end
local tb = {}
local mt ={mm = "test"}
mt.__index = function()
return "not find"
end
setmetatable(tb,mt)
tb.version = "正常版本"
print(tb.version)
print(tb.server) ---不存在的值,调用__index方法
--rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效
print(rawget(tb, "version")) --打印 正常版本
print(rawget(tb, "server")) --打印nil
利用元表的特性实现对象继承
local function class( super )
local cls
if super then
cls = {}
cls.super = super
setmetatable(cls, {__index = super})
else
-- ctor是构造函数的命名
cls = {ctor = function () end}
end
cls.__index = cls
function cls.new( ... )
local instance = setmetatable({}, cls)
instance:ctor()
return instance
end
return cls
end
--测试实现部分
local Test = class()
function Test:doSomething()
print("test doSomething")
end
local test = Test.new()
test:doSomething()
--测试继承部分
local Test = class()
function Test:doSomething()
print("test doSomething")
end
local Test2 = class(Test)
local test = Test2.new()
test:doSomething()
在new的时候,创建一个table并返回,即创建一个实例,实例可以有自己的字段,比如Test类的doSomething,该字段是个函数,可以调用执行。实例的元表是cls,如果调用实例没有的字段,会去cls里找
cls设置了元方法__index = cls
如果没有super,则只有一个构造函数方法
如果有super,cls的元表是super,元表的元方法也正确的设置了
所以,在Test2是继承自Test的,它的实例test调用doSomething,找不到,去元表里找,元表发现自己有父类,去父类里找,成功找到。
多继承
如果我想要继承多个父类,怎么办?
思路就是将元方法改成函数
local function search(key, tables)
for _, super in ipairs(tables) do
if super[key] then
return super[key]
end
end
return nil
end
local function class(...)
local cls = { ctor = function () end}
local supers = {...}
setmetatable(cls, {__index = function (_, key)
-- 在查找table的时候,会把table的key传进来
return search(key, supers)
end})
function cls.new(...)
local instance = {}
setmetatable(instance, {__index = cls})
instance:ctor(...)
return instance
end
return cls
end
local Human = class()
function Human:life()
print("almost 100 years.")
end
local Programmer = class()
function Programmer:coding()
print("sub 1 year.")
end
local My = class(Human, Programmer)
local You = My.new()
You:life()
You:coding()
almost 100 years.
sub 1 year.
解析:
在You里找不到life和coding字段,去找元表cls,调用元方法__index,__index调用函数search,把所有的父类都找一遍
成功找到