Lua学习之metatables and metamethods(二)

接着上一篇博客,继续学习metamethods。按照文档的顺序,接下来是库定义的metamethods。
一.库定义的Metamethods。
简单来说,Lua的很多库都有自己的metatables,在这些metatables定义了许多域,到现在我们看到的metamethods都是Lua核心。虚拟机会不停的检测运行时的值,当这个值涉及到一个操作,这个操作有metatables,并且这些metatables为这个操作定义了metamethods。(这一段是我根据 < Programming in lua,2Nd Edition>翻译的。)
读起来有点拗口,不过比起中文第一版自己觉得还是比较贴切的。
文档上面说了一个例子,print(),我们经常调用print()来输出,我们可以传入字符串参数print(“hello”),也可以传入数值,print(123),也可以传入一个table,print({}),但是都能够输出。这是因为,print()调用了tostring方法,它会把各种类型的参数转换成字符串。以table为例子:

t1 = {1,2,3,4}
print(t1)
print(tostring(t1))

这里写图片描述
发现输出的值是一样的,是表t1的地址。tostring会检查t1时候有metatable,如果有metatable会进一步检查是否有__tostring域。如果有则调用,__tostring必须返回一个字符串。我们为它定义一个metatable和__tostring域。然后调用print()输出。

t1 = {1,2,3,4}
print(t1)

mt = {}
mt.__tostring = function (t)
    local s = "{"
    for _, k in pairs(t) do
        s = s..k.." "
    end
    return s.."}"
end

setmetatable(t1,mt)
print(t1)

这里写图片描述
二.表相关的metamethods
我们常说一个表的域,这个域是什么,它可以是表的一个元素,一个函数。比如:

student = {
    age = 16,
    score = 17,
    SetName = function (s,str)
        s["name"] = str
    end
}

print(student["age"])
print(student["score"])
print(student["name"])
student.SetName(student,"hello")
print(student["name"])

Lua学习之metatables and metamethods(二)_第1张图片
我们访问了age,score,name,三个域,这三个是元素,访问了SetName这个域,这是函数。
从结果也可以看出,第一次student没有name这个域,返回nil,后面一次访问已经有了name这个域,所以输出正确的值。假设我们接下去要访问student更多的域,比如sexual(性别),这时候返回的是nil,假设我们需要它返回一个非nil的值,我们该怎么做呢,这时候,__index metamethod就派上用场了。
1. The __index Metamethod
__index的作用是当我们访问一个表的不存在的域时,它会查找__index这个域是否存在,如果存在由__index metamethod返回结果,如果不存在返回nil。上面的例子并不规范,我们仿造文档上面的例子改进一下:

Student = {}
Student.mt = {}
Student.prototype = { age = 1, birth = 20150325, sexual = 0}

function Student.new (s)
    setmetatable ( s, Student.mt)
    return s
end

Student.mt.__index = function (table, k)
    return Student.prototype[k]
end

s1 = { age = 1, sexual = 1}
print(s1.birth)
setmetatable(s1,Student.mt)
print(s1.birth)

这里写图片描述
需要特别注意的是

Student.mt.__index = function (table, k)

中的参数table不能省略,省略的话,执行的结果是nil
我们说过,表的域可以是函数也可以是其他类型,那么__index域也是一样的,上面的例子中是一个函数,我们把它换成一个表。

Student.mt.__index = Student.prototype

执行结果是一样的。
2.The __newindex Metamethod
这个__newindex metamethod 是用来给表更新用的,而__index是用来访问表的。当你要给表的一个缺省域赋值的时候,解释器就会查找__newindex metamthod,如果找到就执行__newindex metathod,而不进行赋值。和__index一样,如果这个metamethod 是一个表,解释器会对这个表进行赋值,而不是在原来的表上赋值。看个例子:

function output(t)
    for k,v in pairs(t) do
        print(k,v)
    end
end

Student = {}
Student.mt = {}
Student.prototype = { age = 10, birth = 20050326}
function Student.new (s)
    setmetatable(s,Student.mt)
    return s 
end

Student.mt.__newindex = function (table,key,value)
    Student.prototype[key] = value
end

我们定义了一个输出函数,一个Student表,定义了一个metatable Student.mt,给这个metatable也定义了__newidnex域。我们知道如果没有___newindex域,赋值的时候就是直接赋值了:

s1 = {age = 5,birth = 20100301}
output(s1)
s1.sexual = 1
output(s1)

Lua学习之metatables and metamethods(二)_第2张图片
如果表有一个__newindex域,它就会执行__newindex metamethod操作了:

s2 = Student.new{age = 5,birth = 20100301}
output(s2)
s2.sexual = 1   -- assign a value to a absent index
output(s2)

这里写图片描述
可以看出来,没有对s2进行赋值操作,而是执行了__newindex操作,下面我们看看这个操作的结果:

s2 = Student.new{age = 5,birth = 20100301}
--output(s2)
s2.sexual = 1   -- assign a value to a absent index
--output(s2)
output(Student.prototype)

这里写图片描述
如果我们把__newindex域从函数换成Student.prototype执行结果也是一样的。这里就不需要再演示了。
三.__index 和 __newindex的应用
文档里面,列出来了关于这两个metathods的三个应用,有默认值的表,监控表,只读表。我们也看一下这三个应用的详情。
1.有默认值的表。
这是通过__index metathod来实现的。我们看一下文档给出的例子。

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)
setDefault(tab,0)
print(tab.x,tab.z)

通过函数setDefault(t,d)来设置t的默认值,它通过为t创建一个metatable (local mt),并且在这个metatable里定义了__index域。这样做就能达到了,设置默认值的目的。但是,有一个问题,当我们有多个表,并且这些表需要不同的默认值,我们就要多次调用这个函数,就要花费比较大的代价(主要是频繁创建metatable)。文档也给出了解决方案,我们把这个默认值存在了需要设置默认值的表里面。

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

tab = {x = 10,y = 20}
print(tab.x,tab.z)
setDefault(tab,0)
print(tab.x,tab.z)

这个解决方法就是,所有调用setDefault(t,d)方法之后,这些表都有相同的metatable mt,并且它们的默认值也是不一样的。这个例子中,每个表存放默认值的域是’_‘,
如果担心命名混乱,可以新建一个表作为这个域。我们知道很多数据类型都可以做为table的键值,table也可以作为table的键值,所以文档的最后一个例子,是使用一个表来代替’_‘作为键值。
2.监控表
监控一个表的访问情况,比如被访问了哪些元素,更新了那个元素的值。实现原理是:假设我们想监控t,我们监控t的代理,它的代理是一个空表,空表是不存在域的,所以当我们访问空表的任何域时,解释器都会去查找__index方法。我们给出文档上面的例子:

-- create a private index
t = {}
local index = {}
-- create metatable
local mt = {

    __index = function (t,k)
        print("*access to element "..tostring(k))
        return t[index][k]
    end,

    __newindex = function (t,k,v)
        print("*update to 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

t = track(t)

t[1] = 'Hello'
print(t[1])
t[2] = 'Are'
print(t[2])

Lua学习之metatables and metamethods(二)_第3张图片
在这个例子中,t是我们想要监控的表,我们创建了一个代理local proxy,并且为这个代理设置一个metatable mt,在这个mt里面定义了__index和__newindex 两个metamethods。
3.只读表
只读的意思是,可以访问,但是不能更新里面的值,也不能增加新的域,赋新的值。实现方法是,当检测到__newindex被调用时,就是表被更新了,这时候抛出错误就可以了。文档上面的例子:

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])      --> can read
days[2] = "Noday"   --> can't assign

你可能感兴趣的:(Lua)