lua入门笔记3 元表(metatable)与元方法(metamethod)

  • 元表的概念

    通常,lua中的每个值都会有一套预定义的操作集合。例如可以将两个数字相加,但是我们没有办法直接让两个table相加,也没有办法对函数作比较,或者调用一个字符。但是我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。

    例如,我们有两个table a,b 可以通过元表定义出如何计算a+b。当lua试图将两个table相加时,他会先检查二者之一是否有元表,然后检查该元表中是否有一个叫_add的字段。如果找到了该字段,就调用该字段对应的值,这个值也就是所谓的元方法。也就是c++中的运算符重载

  • 注意事项

  1. lua里每一个值都有一个元表,不同的是tableuserdata可以有各自独立的元表(两个table可以分别对应两个元表),而其他类型的值则共享其类型所属的单一元表。
t={}
print(getmetatable(t))   -->nul  lua在创建新的table时,不会创建元表

t1={}
t.setmetatable(t,t1)      -->将t1设置为t的元表  
  1. 任何的table都可以作为任何值的元表,一组table可以共用一个元表。这个元表描述了他们共同的行为。一个table甚至可以作为他自己的元表,用于描述他自己特有的行为。

  2. lua里,你只能设置table的元表。如果想设置其他的类型必须通过C代码来实现。

  • 算数类的元方法

我们这里先看一个示例

Set={}

mt={}

function Set.New(t)
    local res={}
    setmetatable(res,mt)
    for k,v in ipairs(t) do
        res[k]=v;
    end
    return res
end

function Set.Add(a,b)
    local res=Set.New({})
    for k,v in ipairs(a)    do res[k]=v end
    for k,v in ipairs(b)    do res[k]=v end
    return res
end


function Set.PrintToString(a)
    res="{ "
    for k,v in ipairs(a)   do   res=res..v.." " end
    res=res.." }"

    return res
end

mt.__add=Set.Add
mt.__concat=Set.PrintToString


a=Set.New({1,2,3,4,5})
b=Set.New{5,6,7}

c=a+b
print(Set.PrintToString(c))

输出结果为 { 5 6 7 4 5 }
这里是一个简单地案例,修改了两个table相加的元方法。
这里需要注意的是,当两个集合相加时,可以使用任意一个集合的元表。然而,当一个表达式中混合了不同元表的值时,他会按照以下步骤查找元表:如果第一个有元表,并且元表中有__add字段,lua就以此字段作为元方法;如果两个值都没有元方法,Lua就引发一个错误
同理,我们也可以在此基础上对其他的操作进行重载,例如减法,乘法等...

  • 关系类的元方法

关系类的元方法有三个,分别为__eq(等于)_lt(小于)__le(小于等于),其他三个没有单独的方法,通过一下转换得出。

a~=b  ==> not(a==b)
a>b  ==>  b=b  ==>  b<=a 

与算术类元方法不同的是,关系类元方法不能应用于混合类型。如果试图这样操作,lua会引发一个错误
等于比较永远不会引发错误。但是如果两个对象拥有了不同的元方法,那么等于操作不会调用任何一个元方法,而是直接返回false。只有当两个比较对象共享一个元方法是,lua才调用这个等于比较的元方法。

  • 库定义的元方法

    目前介绍的元方法只针对lua的核心,也就是一个虚拟机。由于元表也是一种常规的table,所以任何人、任何函数都可以使用他们。
    函数print总是调用tostring来格式化其输出。(tostring ==> __tostring)
    函数setmetatablegetmetable也会用到用到元表中的一个字段,用于保护元表。假设想要保护器元表,使用户及看不见也不能修改集合的元表,那么就需要用到字段__metatable。当设置了该字段时,getmetatable就会返回该段的值,而setmetatble会引发一个错误。

  • table访问的元方法

lua还提供了一种可以改变table行为的方法。有两种可以改变的table行为:查询table以及修改table中不存在的字段。

1. __index元方法

当访问一个table中不存在的字段时,得到的结果是nil。但实际上,这些访问会促使解释器去查找一个叫__index的元方法。如果没有这个元方法,那么访问结果如前所述的为nil。否则,就由这个元方法来提供最终结果。

来看一个比较经典的示例

Window={}
Window.prototype={x=0,y=0,width=100,height=100}
Window.mt={}

Window.mt.__index=function(table,key)
    return Window.prototype[key]
end

function Window.new(o)        --类似于构造函数
    setmetatable(o, Window.mt)
    return o
end

print((Window.new{x=10,y=20}).width)

这里要创建一些窗口的table,每个table中必须描述一些窗口参数,所有的这些参数都有默认值。当没有给对应参数赋值是,返回默认值。这里相当于是一个简单地继承。所有生成的窗口继承了prortotype

这个示例中__index元方法是一个函数。但其实他还可以是一个table。当他是一个table时,lua就会在这个table中查询这个key所对应的的value

虽然将函数作为__index 来实现相同功能开销较大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其他一些功能。
如果不想在访问一个table时涉及到他的__index元方法,可以使用函数rawget调用rawget(t,i)就相当于对table进行了一个原始的访问,不考虑元表

2.__newindex元方法

__newindex元方法与__index类似,不同之处在于前者用于table的更新(set)后者用于table的查询(get)。当对一个table中不存在的索引赋值时,解释器就会查找__newindx元方法。如果有,解释器就会调用它而不是执行赋值。

3.具有默认值的table

常规table中的任何字段默认都是nil。通过元表我们可以很容易地修改这个默认值:

function setDefault(t,d)
  local mt=(__index=function() return d end)
  setmetatable(t,mt)
end

tab ={x=10,y=20}
print(tab.x,tab.z)    -->10  nil
setdefalut(tab,0)
print(tab.x,tab.z)    -->10 0

在调用setdefault后,任何对tab中存在字段的访问都将调用他的__index元方法,而这个元方法会返回0(这个原方法中d的值)
这里setDefault函数为了所有需要默认值的table创建了一个新的元表,如果需要很多带默认值的table其开销会比较大。这里我们可以换一种写法
这里我们把 默认值存放在table本身中,_ _ _用这种相对特殊的命名防止冲突

local mt={__index=function(t) return t.___ end}
function setDefault(t,d)
  t.___=d
  setmetatable(t,mt)
end

防止冲突还有一种写法,确保这个特殊key的唯一性

local key={}      --唯一key

local mt={__index=function(t) return t.[key] end}
function setDefault(t,d)
  t.[key]=d
  setmetatable(t,mt)
end

4. 跟踪table的访问

_index_newindex 都是在table中没有需要访问的index才发挥作用的。因此只有讲一个table保持空,才可能捕捉到所有对他的访问。

t={}    --原来的table
local _t=t;     --保持一个对原来的table的引用
t={}            --创建代理

local mt={
    __index=function(t,k)
        print("get element"..k)
        return _t[k]      --从本来的table中获取get数值
    end,
    __newindex=function(t,k,v)
        print("set element"..k)
        _t[k]=v            --像本来的table中set数值
    end
}
setmetatable(t, mt)

t[1]=1
t[2]=2
print(t[1])

输出结果:

set element1
set element2
get element1
1

但这种写法有一个弊端:无法遍历原本的table。pairs只能够访问到代理的table

但是这里又有一个问题。如果我们需要同时监视多个table,其实无需创建多个不同的元表。我们只要以某种形式让每个代理和元表关联起来,并且所有代理都共享一个公共的元表,这里上代码

local index={}                  --当做一个不容易重复的key值

local mt={
    __index=function(t,k)
        print("get data"..k)
        return t[index][k]
    end,
    __newindex=function(t,k,v)
        print("set data"..k)
        t[index][k]=v
    end
}

function track(t)
    local proxy={}                  --此处每次都会创建新的table,此table会作为代理返回
    print("proxy adress is "..tostring(proxy))
    print("index adress is "..tostring(index))
    proxy[index]=t                  --真正的数据,放在table内某个key值能难重复的地方
    setmetatable(proxy, mt)         
    return proxy
end

t={}
t=track(t)
t[1]=45
print(t[1])
t1={}
t1=track(t1)
print(t1[1])

所有的代理都存放在元表内,而真正的表又在代理内。外部无法直接获取到对应的值

5. 只读的table

我们可以通过代理概念,很轻松的创造出实现只读的元表。具体只需要在__newindex是取消更新操作并引发一个错误提示。具体代码不在演示。


最后所有重载的一览

函数名 意义&注意事项
_add + 操作,如果任何不是数字的值(包括不能转换为数字的字符串)做加法, Lua 就会尝试调用元方法。 首先、Lua 检查第一个操作数(即使它是合法的), 如果这个操作数没有为 "__add" 事件定义元方法, Lua 就会接着检查第二个操作数。 一旦 Lua 找到了元方法, 它将把两个操作数作为参数传入元方法, 元方法的结果(调整为单个值)作为这个操作的结果。 如果找不到元方法,将抛出一个错误。
__sub - 操作。 行为和 "add" 操作类似。
__mul * 操作。 行为和 "add" 操作类似。
__div / 操作。 行为和 "add" 操作类似
__mod % 操作。 行为和 "add" 操作类似
__pow ^ (次方)操作。 行为和 "add" 操作类似
__unm - (取负)操作。 行为和 "add" 操作类似。
__concat .. (连接)操作。 行为和 "add" 操作类似, 不同的是 Lua 在任何操作数即不是一个字符串 也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。
__idiv // (向下取整除法)操作。 行为和 "add" 操作类似。
__band & (按位与)操作。 行为和 "add" 操作类似, 不同的是 Lua 会在任何一个操作数无法转换为整数时 (参见 §3.4.3)尝试取元方法。
__bor |(按位或)操作。 行为和 "band" 操作类似。
__bxor ~ (按位异或)操作。 行为和 "band" 操作类似。
__bnot ~ (按位非)操作。 行为和 "band" 操作类似。

lua手册

你可能感兴趣的:(lua入门笔记3 元表(metatable)与元方法(metamethod))