Lua_元表详解_深入理解index与newindex_自实现监控表_只读表_运算符重载_tostring(14)


码云代码链接
https://gitee.com/wenwenc9/lua_pro.git

元表概念

在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作
因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。

当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"__add"的字段,若找到,则调用对应的值。"__add"等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。
有两个很重要的函数来处理元表:

  • setmetatable(table,metatable): 对指定table设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败 。[不能多继承]
  • getmetatable(table): 返回对象的元表(metatable)。

以下实例演示了如何对指定的表设置元表:

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

尝试用上面查找元素规则理解!!!!

三、__index元方法(查找方法)

1 运用__index创建元表

只有使用这个方法才能实现真正意义元表,单存setmetatable并不会找到,所想继承的元表元素

other = { foo = 3}
t = setmetatable({},{__index = other})
print(t.foo) -- 3
print(t.bar) -- nil

2 __index对应函数

如果__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 表赋值为 {key1 = “value1”}。
  • mytable 设置了元表,元方法为 __index。
  • 在mytable表中查找 key1,如果找到,返回该元素,找不到则继续。
  • 在mytable表中查找 key2,如果找到,返回该元素,找不到则继续。
  • 判断元表有没有__index方法,如果__index方法是一个函数,则调用该函数。
    元方法中查看是否传入 “key2” 键的参数(mytable.key2已设置),如果传入 “key2” 参数返回 “metatablevalue”,否则返回 mytable 对应的键值。

上面代码可以简写

mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)

当我们访问一个表的不存在的域,返回结果为 nil,这是正确的,但并不一定正确。
实际上,这种访问触发 lua 解释器去查找__index metamethod:如果不存在,
返回结果为 nil;如果存在则由__index metamethod 返回结果

3 窗口案例

这个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描
述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的值,
当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一
种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认值。第二种方法是,创建一个新的窗口去继承一个原型窗口的缺少域。首先,我们实现一个原型和一
个构造函数,他们共享一个 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元方法(更新与修改)

__newindex 元方法用来对表更新,__index则用来对表访问 。

1 基本示例

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进行查找,发现有对应的键,那么对此表进行更新值

运用rawset绕过metamethod更新表而非元表

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"

五、构建默认值表

方案1

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

方案2

案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.__)

方案3

如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创
建一个新表用作键值:(因为涉及到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)

方案 4

保证统一性,将各个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

方案 5

上面的方法已经很好了,但是假设有成千上万个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 都是只有当表中访问的域不存在时候才起作用。

  • 捕获对一个表的所有访问情况的唯一方法就是保持表为空。
  • 想监控一个表的所有访问情况,应该为真实的创建一个代理。
  • 这个代理就是一个空表,并且带有__index和__newindex 元方法,由这2个方法负责跟踪表的所有访问情况并将其指向原始的表。
  • 假定,t是我们想要跟踪的原始表,我们可以:
--原始表
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 进行操作,而不是原始的表)

  • 如果我们想监控多张表,我们不需要为每一张表都建立一个不同的metatable。
  • 我们只要将每一个 proxy 和他原始的表关联,所有的 proxy 共享一个公用的metatable 即可。
  • 将表和对应的 proxy 关联的一个简单的方法是将原始的表作为 proxy 的域,只要我们保证这个域不用作其他用途。
  • 一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的 key。

将上面的思想汇总,最终的结果如下:

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

七、只读表

  • 采用代理的思路很容易实现一个只读表。
  • 我们需要做得只是当我们监控到企图修改表时候抛出错误。
  • 通过__index metamethod,我们可以不使用函数而是用原始表本身来使用表,因为我们不需要监控查寻。
  • 这是比较简单并且高效的重定向所有查询到原始表的方法。
  • 但是,这种用法要求每一个只读代理有一个单独的新的 metatable,使用__index指向原始表:
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"

八、为表添加操作符

在这里插入图片描述

1 __tostring

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) -- 老王

2 __call

当子表被当做一个函数来使用时,会默认调用这个__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
在这里插入图片描述

3 __add

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 和 __newindex

__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)
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值