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<a
a>=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手册

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容