Lua学习笔记 第十三章 元表(metatable) 与元方法(meatmethod)

通常Lua中,每个值都有一套预定义的操作集合。这个集合叫做元表。

我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。

如两个table相加。

元方法是元表中的一个值,这个值应该是一个函数。

Lua中的每个值都有一个元表。table和userdata可以有各自独立的元表,而其他类型的值则共享其

类型所属的单一元表。Lua在创建新的table时不会创建元表:

t = {}

print(getmetatable(t))      —> nil

但是可以使用setmetatable来设置或修改任何table的元表。

任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表描述了

它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。

在Lua代码中,只能设置table的元表。若要设置其他类型值的元表,则必须通过C代码来完成。在标

准字符串程序库中为所有的字符串都设置了同一个元表,而其他类型在默认情况下都没有元表。

 

13.1 算术类的元方法

通过示例说明如何使用元表。假设用table来表示集合,并且有一些函数用来计算集合的并集和交集等。

将这些函数存入一个名为Set的table中。

Set = {}

function Set.new(list)

    local set ={}

    for k, v inpairs(list) do set[v] = true end

    return set

end

function Set.union(a, b)

    local res =Set.new{}

    for k inpairs(a) res[k] = true end

    for k inpairs(b) res[k] = true end

    return res

end

 

function Set.intersection(a, b)

    local res =Set.new{}

    for k inpairs(a) do

        res[k] =b[k]

    end

    return res

end


--打印集合的函数

function Set.tostring(set)

    local list ={}

    for e inpairs(set) do

        list[#list+ 1] = e

    end

    return"{" .. table.concat(list, ",") .. "}"

end

function Set.print(s)

    print(Set.tostring(s))

end

 

假设使用加号来计算两个集合的并集,那么就需要让所有用于表示集合的table共享一个元表,

并且在该元表中定义如何执行一个加法操作。

第一步,创建一个常规的table用作集合的元表:

    local mt = {}

第二步,修改Set.new函数,将mt设置为当前所创建的table的元表.

    functionSet.new(list)

        local set= {}

setmetatable(set,mt)

        for k, vin pairs(list) do set[v] = true end

        return set

    end

最后,将元方法加入元表中。

    mt.__add =Set.union

还可以用乘号来求集合的交集: mt.__mul = Set.intersection

在元表中,每种算术操作符都有对应的字段名。除了__add 和 __mul外,还有__sum(减法)、

__div(除法)、__unm(相反数)、__mod(取模)和__pow(乘幂),还有__concat用于描述连接操作符的行为。

当两个集合相加时,可是使用任意一个集合的元表。但当两个相加的类型其中一个没有元表时,Lua会

按照如下步骤查找元表:

如果一个值有元表,并且元表中有__add字段,那么Lua就以这个字段为元方法,而与第二个值无关;

反之,如果第二个值有元表并含有__add字段,Lua就以此字段为元方法;如果有两个值都没有元方法,

Lua就会引发一个错误。

 

13.2 关系类的元方法

元表还可以指定关系操作符的含义,元方法为__eq(等于)、__lt(小于)、和__le(小于等于)。而其它3个关系

操作符则没有单独的元方法。

Lua会将 a ~= b转化为not (a==b),将a > b转化为 b < a,将a >= b 转化为 b <= a.

在集合操作中 <= 通常表示集合间的包含关系: a <=b通常意味着a是b的一个子集。

根据这样的提示,仍有可能得到 a <= b 和 b <a 同时为假的情况。

因此需要分别为__le(小于等于) 和 __lt(小于) 提供实现:

mt.__le = function(a, b)

    for k inpairs(a) do

        if notb[k] then return false end

    end

    return true

end

mt.__lt = function(a, b)

    return a<= b and not (b <= a)

end

还可以定义集合的相等性判断:

mt.__eq = function(a, b)

return a <= b and b <= a

end

有了这些定义后,就可以比较集合了。

 

注意:与算术类元方法不同的是,关系类的元方法不能应用于混合的类型。如果试图将一个字符串

与一个数字作顺序性比较,Lua会引发一个错误。同样,如果试图比较有个具有不同元方法的对象,

Lua也会引发一个错误。

等于比较永远不会引发错误。但如果两个对象拥有不同的元方法,那么等于操作不会调用任何一个

元方法,而是直接返回false。只有当两个比较对象共享一个元方法时,Lua才会调用这个等于比较的元方法。

 

13.3 库定义的元方法

在操作某个值时,Lua虚拟机首先会检测这个值是否有元表,元表中是否定义了关于此操作的元方法。

由于元表也是一种常规的table,所以任何人,任何函数都可以使用它们。函数tostring就是一个典型的实例。

函数print总是调用tostring来格式化其输出。当格式化任意值时,tostring会检查该值的元表中是否有一个

__tostring的元方法。如果有这个元方法,tostring就用该值作为参数来调用这个元方法。接下来由这个元方法

完成实现的工作,它返回的结果也就是tostring的结果。

设置示例元表中的__tostring字段:

mt.__tostring = Set.tostring

这样只要调用print来打印集合,print就会调用tostring函数,进而调用到Set.tostring:

s1 = Set.new{10, 4, 5}

print(s1) --> {4, 5, 10}

 

函数setmetatable 和 getmetatable 也会用到元表中的一个字段,用于保护元表。假设

想要保护集合的元表,使用户既不能看也不能修改集合的元表,那么就需要用到字段__metatable。

当设置了该字段时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。

 

13.4 table访问的元方法

算术类和关系类运算符的元方法都为各种错误情况定义了行为,它们不会改变语言的常规

行为。但Lua还提供了可以改变table行为的方法:查询table及修改table中不存在的字段。

13.4.1

当访问一个table中不存在的字段时,得到的结果为nil。这是对的,但并不完全正确。

这种访问会促使解释器去查找一个叫__index的元方法。如果没有这个元方法,访问结果就是nil。

否则就由这个元方法来提供最终的结果。

一个有关继承的典型示例。要创建一些描述窗口的table,每个table中必须描述一些窗口参数,

例如位置、大小及主题颜色等,这些参数都有默认值。在创建窗口对象时还可以仅指定那些不同于默认值的参数。

第一种方法是使用一个构造式,在其中填写那些不存在的字段。

第二种方法是让新窗口从一个原型窗口处继承所有不存在的字段。首先,声明一个原型和一个构造函数,构造函数创建新的窗口,并使他们共享同一个元表:

Window = {}

Window.prototype = {x=0, y=0, width=100, height=100} -- 使用默认值创建原型

Window.mt = {}                              --创建元表

function Window.new(o)                      --声明构造函数

    setmetatable(o,mt)

    return o

end

定义 __index 元方法:

Window.mt.__index = function(table, key)

    returnWindow.prototype[key]

end

创建一个新窗口,查询一个它没有的字段:

w = Window.new{x=10, y=20}

print(w.width)     ---> 100

 

在Lua中,将__index元方法用于继承是很普遍的方法,但Lua还提供了一种更便捷的方式来实现此功能。

__index元方法不一定是一个函数,它还可以是一个table。当它是一个函数时,Lua以table和一个不存在

的key作为参数来调用该函数;当它是一个table时,Lua就以相同的方式来重新访问这个table。因此,

前例中__index的声明可以简单地写为:

Window.mt.__index = Window.prototype

将一个table作为__index的元方法是一种快捷的、实现单一继承的方式。虽然将函数作为__index来实现相

同功能的开销较大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其它一些功能。

第16章中详细讨论。

如果不想在访问一个table时涉及到它的__index元方法,可以使用函数rawget.调用rawget(t,i)就是对table进

行了一个原始的访问,也就是一次不考虑元表的简单访问。但一次原始访问并不会加速代码的执行。

  

13.4.2 __newindex 元方法

__newindex元方法与__index类似,不同之处在于前者用于table的更新,而后者用于table的查询。当对一

个table中不存在的索引赋值时,解释器就会查找__newindex元方法。如果有这个元方法,解释器就调用它,

而不是执行赋值。如果这个元方法是一个table,解释器就在此table中执行赋值,而不是对原来的table。

此外还有一个原始函数允许绕过元方法:调用rawset(t,k,v)就可以不涉及任何元方法而直接设置table t中的key k

 相关联的value v。

组合使用__index和__newindex元方法可以实现出Lua中的强大功能,如:只读的table、具有默认值的table和

面向对象编程中的继承。

 

13.4.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

setDefault(tab, 0)

print(tab.x, tab.z)     -->10 0

setDefault函数为所有需要默认值得table创建了一个新的元表。但如果创建很多需要不同默认值的table,

这种方法的开销就比较大。这时,可以使用一个额外的字段来保持默认值,如可以使用"___"这个key

作为额外的字段:

local mt = {__index = function(t) return t.___ end}

function setDefault(t, d)

    t.___ = d

    setmetatable(t,mt)

end

如果担心名称冲突,那么要确保这个特殊key的唯一性,只需创建一个新的table并用它作为key即可:

local key = {}

local mt = {__index = function(t)return t[key] end}

function setDefault(t, d)

    t[key] = d

setmetatable(t, mt)

end

还有一种方法可以将table与其默认值关联起来:使用一个独立的table,它的key为各种table,value就是

各个table的默认值。(需要使用弱引用,见17章)

还有一种备忘录元表的方法,它能使具有相同默认值的table复用同一个元表。(需要使用弱引用)

 

13.4.4 跟踪table的访问

__index 和 __newindex 都是在table中没有所需访问的index时才发挥作用。因此只有将一个table保持为空,

才有可能捕捉到所有对它的访问。为了监视一个table的所有访问,就应该为真正的table创建一个代理。

这个代理就是一个空的table,其中__index和__newindex元方法可用于跟踪所有的访问,并将访问重新

定向到原来的table上。示例——跟踪table t 的访问:

t = {}          -- 原来的table

local _t = t    --保持对原table的一个私有访问

t = {}          -- 创建代理

local mt = {

    __index =function(t, k)

        print("access to element"..tostring(k))

        return_t[k]    -- 访问原来的table 

    end,

    __newindex =function(t, k, v)

        print("update of element"..tostring(k).." to "..tostring(v))

        _t[k] = v      --更新原来的table

    end

}

setmetatable(t,mt)

但这种方法无法遍历原来的table。函数pairs只能操作代理table,无法访问原来的table。

如果要同时监视几个table,无须为每个table创建不同的元表。相反,只要以某种形式将每个代理

与其原table关联起来,并且所有代理都共享一个公共的元表。例如将原来的table保存在代理table的

一个特殊字段中。代码如下:

local index = {}            -- 创建私有索引

local mt = {

    __index =function(t, k)

print("access toelement "..tostring(k))

        returnt[index][k]  -- 访问原来的table

    end,

    __newindex =function(t, k, v)

print("updateof element " ..tostring(k).." to "..tostring(v))

        t[index][k]= v     -- 更新原来的table

    end

}

function track(t)

    local proxy ={}

    proxy[index]= t

    setmetatable(proxy,mt)

    return proxy

end

现在要监视table t,唯一要做的就是执行: t =track(t)。

 

13.4.5 只读的table

通过代理的概念,可以很容易地实现出只读的table。只需要跟踪所有对table的更新操作,并引发一个

错误就可以。如果无须跟踪查询访问,对于__index元方法可以直接使用原table来代替函数。这种做法简单,

效率高,但是要求为每个只读代理创建一个新的元表,其中__index指向原来的table。示例代码:

function readOnly(t)

    local proxy ={}

    local mt = {

        __index =t,

        __newindex= function(t, k, v)

            error("attemptto update a read-only table", 2)

        end

    }

    setmetatable(proxy,mt)

    return proxy

end

使用示例,创建了一个表示星期的只读table:

days =readOnly{"Sunday","Monday", "Tuesday","Wednesday", "Thursday", "Friday","Saturday"}

print(days[1])      --> Sunday

days[2] = "Noday"   --> stdin:1:1 attempt to update a read-only table


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值