Programming in Lua, 2Nd Edition - Chapter 13: Metatables and Metamethods

 

 

Chapter 13: Metatables and Metamethods

 

我们可以加两个数,连接字符串,往表中插入key-value 对,但我们不能对表执行加法,不能比较两个函数,不能call 字符串。

 

元表(Metatables) 充许我们改变值的行为,当通到未定义操作。例如,使用元表我们能定义Lua 如何计算“表a + blua 会检查它们是否有metatable,这个metatable 是抵抗有一个__add 域。如果找到这个域,就调用相对应的值,既元方法metamethod。由元方法完成加法计算。

 

Lua 中所有值都可以有元表。Table userdata 拥有独立的元表。其它类型的值共享单个元表。

 

新创建的表不带元表

 

t = {}

print(getmetatable(t)) --> nil

 

为表设置元表

 

t = {}

print(getmetatable(t)) --> nil

t1 = {}

setmetatable(t, t1)

assert(getmetatable(t) == t1)

print(getmetatable(t)) --> table: 003CACD0

 

一组相关表可以共享同一个元表,它描述了这些表的共同行为。表可以作为自已的元表。

 

Lua 中只能用table 作为元表,如果使用C 代码则可突破此限制。第20 章我们会看到,string 库为string 设置了元表,所有其它类型默认没有元表

 


 

13.1 Arithmetic Metamethods

 

这一节,我们引入一个简单的例子解释如何使用元表。假设我们使用表来表示集合,并用函数去计算两集合的并集,交集,等。为保持命名空间的清洁,我们存储这些函数在表的内部

 

Set = {}

-- create a new set with the values of the given list

function Set.new (l)

       local set = {}

       for _, v in ipairs(l) do set[v] = true end

       return set

end

 

function Set.union (a, b)

       local res = Set.new{}

       for k in pairs(a) do res[k] = true end

       for k in pairs(b) do res[k] = true end

       return res

end

 

function Set.intersection (a, b)

       local res = Set.new{}

       for k in pairs(a) do

              res[k] = b[k]

       end

       return res

end

 

function Set.tostring (set)

       local l = {} -- list to put all elements from the set

       for e in pairs(set) do

              l[#l + 1] = e

       end

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

end

 

function Set.print (s)

       print(Set.tostring(s))

end

 

现在,我们想用操作符+ 计算两个集合的并。我们要让所有支持这种操作的表共享同一个元表。第一步,创建一个普通的表作为所有这些表的元表

 

local mt = {} -- metatable for sets

 

第二步,修改Set.new,让它创建表后设置其元表为mt

 

function Set.new (l) -- 2nd version

       local set = {}

       setmetatable(set, mt)

       for _, v in ipairs(l) do set[v] = true end

       return set

end

 

最后,为元表添加元方法__add 域描述了如何执行加法操作:

 

mt.__add = Set.union

s3 = s1 + s2

Set.print(s3) --> {1, 10, 20, 30, 50}

 

类似的,可以添加乘操作:

 

mt.__mul = Set.intersection

Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}

 

 

基于元表的“运算符重载”

 

Set = {}

local mt = {} -- metatable for sets

-- create a new set with the values of the given list

function Set.new (l) -- 2nd version

       local set = {}

       setmetatable(set, mt)

       for _, v in ipairs(l) do set[v] = true end

       return set

end

 

function Set.union (a, b)

       if getmetatable(a) ~= mt or getmetatable(b) ~= mt then     -- 检查a,b 是否具有相同的元表

              error("attempt to 'add' a set with a non-set value", 2)

       end

       local res = Set.new{}

       for k in pairs(a) do res[k] = true end

       for k in pairs(b) do res[k] = true end

       return res

end

 

function Set.intersection (a, b)

       local res = Set.new{}

       for k in pairs(a) do

              res[k] = b[k]

       end

       return res

end

 

function Set.tostring (set)

       local l = {} -- list to put all elements from the set

       for e in pairs(set) do

              l[#l + 1] = e

       end

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

end

 

function Set.print (s)

       print(Set.tostring(s))

end

 

s1 = Set.new{10, 20, 30, 50}

s2 = Set.new{30, 1}

print(getmetatable(s1)) --> table: 00672B60

print(getmetatable(s2)) --> table: 00672B60

mt.__add = Set.union

s3 = s1 + s2

Set.print(s3) --> {1, 10, 20, 30, 50}

mt.__mul = Set.intersection

Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}

 


 

13.2 Relational Metamethods

 

Set = {}

local mt = {} -- metatable for sets

-- create a new set with the values of the given list

function Set.new (l) -- 2nd version

       local set = {}

       setmetatable(set, mt)

       for _, v in ipairs(l) do set[v] = true end

       return set

end

 

function Set.union (a, b)

       if getmetatable(a) ~= mt or getmetatable(b) ~= mt then     -- 检查a,b 是否具有相同的元表

              error("attempt to 'add' a set with a non-set value", 2)

       end

       local res = Set.new{}

       for k in pairs(a) do res[k] = true end

       for k in pairs(b) do res[k] = true end

       return res

end

 

function Set.intersection (a, b)

       local res = Set.new{}

       for k in pairs(a) do

              res[k] = b[k]

       end

       return res

end

 

function Set.tostring (set)

       local l = {} -- list to put all elements from the set

       for e in pairs(set) do

              l[#l + 1] = e

       end

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

end

 

function Set.print (s)

       print(Set.tostring(s))

end

 

mt.__le = function (a, b) -- set containment

       for k in pairs(a) do

              if not b[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

 

 

mt.__add = Set.union

mt.__mul = Set.intersection

 

s1 = Set.new{2, 4}

s2 = Set.new{4, 10, 2}

print(s1 <= s2) --> true   -- s1 s2 的子集

print(s1 < s2) --> true

print(s1 >= s1) --> true

print(s1 > s1) --> false

print(s1 == s2 * s1) --> true

 


 

13.3 Library-Defined Metamethods

 

printf 首先检查传给它的值是否存在元方法__tostring

 

print({}) --> table: 0x8062ac0

 

mt.__tostring = Set.tostring

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

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

 

 

保护元表

 

mt.__metatable = "not your business"

s1 = Set.new{}

print(getmetatable(s1)) --> not your business

setmetatable(s1, {})

stdin:1: cannot change protected metatable

 


 

13.4 Table-Access Metamethods

 

The __index metamethod

 

通常访问表中不存在的域会得到nil 值。而实际上,在内部会查找__index 元方法,如果确实不存在__index 方法就返回nil,否则返回值就由这个元方法给出。

 

Window = {} -- create a namespace

-- create the prototype with default values

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

Window.mt = {} -- create a metatable

 

-- declare the constructor function

function Window.new (o)

       setmetatable(o, Window.mt)

       return o

end

 

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

       return Window.prototype[key]

end

 

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

print(w.width) --> 100

 


 

__index 元方法既可以是一个函数也可以是一个表

 

Window = {} -- create a namespace

-- create the prototype with default values

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

Window.mt = {} -- create a metatable

 

-- declare the constructor function

function Window.new (o)

       setmetatable(o, Window.mt)

       return o

end

 

Window.mt.__index = Window.prototype

 

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

print(w.width) --> 100       -- w.width 相当于访问 Window.prototype["width"]

 

 

使用表作为 __index metamethod 提供了一种快速简单的方法实现单继承。使用函数虽然开销大些,也更灵活:可以实现多继承caching,和其它变化形式。第16 章会讨论这些变化形式。

 

rawget 函数实现非__index 访问

 

rawget(t,i) ,  raw access 不会提高速度,但有时你会需要它,以后会看到。

 

 

The __newindex metamethod

 

t[“key”] = value ”key” 这个索引不存在时,内部会查看是否存在__newindex 元方法,如果有则调用这个元方法来代替赋值操作。__newindex 也可以是表,如果是这样这个赋值操作就是作用于这个表的,而不是原来那个。同样,rawset(t,k,v) 是不考虑元方法的赋值操作。

 

组合使用__index __newindex 元方法可以实现只读表,带默认值的表,面向对象的继承等。面象对象编程有专门的章节讨论。

 

具有默认值的表

 

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

 

 

同一个元表不同默认值

 

可以让各个表自已存自已的默认值。如果不担心命名冲突可以给每个表额外加一个___域:

 

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

function setDefault (t, d)

       t.___ = d

       setmetatable(t, mt)

end

 

不会产生命名冲突的解决方案要等到第17 章,因为需要weak tables 或是memoize metatables

 

local key = {} -- unique key

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

function setDefault (t, d)

       t[key] = d

       setmetatable(t, mt)

end


 

Tracking table accesses

 

追踪对表的访问需要一个“代理表”来代理对真实表的所有访问。代理表是一个空表,有适当的__index __newindex 元方法,它跟踪所有访问并将它们重定向到原始表。假设

t 是我们想要追踪的原始表:

 

t = {} -- original table (created somewhere)

-- keep a private access to the original table

local _t = t

-- create proxy

t = {}

-- create metatable

local mt = {

__index = function (t, k)

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

       return _t[k] -- access the original table

  end,

__newindex = function (t, k, v)

       print("*update of element " .. tostring(k) ..

              " to " .. tostring(v))

       _t[k] = v -- update original table

  end

}

setmetatable(t, mt)

 

t[2] = "hello"

print(t[2])

 

(注意到,很不幸的,这种机制使得我们无法遍历表,pairs 函数将作用于代理表,而不是原始表)

 

如果我们想要监视多个表,我们不需要给每个表都设置不同的元表。将原始表存在代理表里面:

 

监视多个表

 

t = {}

t2 ={}

local index = {} -- create private index

local mt = { -- create metatable

       __index = function (t, k)

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

              return t[index][k] -- access the original table

         end,

       __newindex = function (t, k, v)

              print("*update of element " .. tostring(k) ..

              " to " .. tostring(v))

              t[index][k] = v -- update original table

         end

}

 

function track (t)

       local proxy = {}

       proxy[index] = t

       setmetatable(proxy, mt)

       return proxy

end

 

t=track(t)

t2=track(t2)

print(t[1])

print(t2[2])

 

 

Read-only tables

 

只读表

 

function readOnly (t)

       local proxy = {}

       local mt = { -- create metatable

       __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" -- stdin:1: attempt to update a read-only table

 

你可能感兴趣的:(lua)