元表和元方法

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

基本的metatable

  • 创建新的table时不会创建元表
  • getmetatable(table) 获取table或者userdata类型变量的元表
  • setmetatable(t,ot) 设置table或者userdata类型变量的元表

    1
    2
    3
    4
    5
    6
    local t = {1,1}
    print getmetatable(t) -- nil
    local t1 = {}
    setmetatable(t,t1)
    assert(getmetatable(t) == t1)
  • Lua代码中,只能设置table的元表,若要设置其他类型的值的元表,必须通过C代码来完成。

  • 标准的字符串程序库为所有的字符串都设置了一个元表,其他类型在默认情况下没有元表。
  • table中可以重新定义的元方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    __add(a,b) --加法
    __sub(a,b) --减法
    __mul(a,b) --乘法
    __div(a,b) --除法
    __mod(a,b) --取模
    __pow(a,b) --乘幂
    __unm(a) --相反数
    __concat(a,b) -- 连接
    __len(a) --长度
    __eq(a,b) --相等
    __lt(a,b) --小于
    __le(a,b) --小于等于
    __index(a,b) --索引查询
    __newindex(a,b,c) --索引更新
    __call(a,...) --执行方法调用
    __tostring(a) --字符串输出
    __metatable --保护元表
  • 当操作符的两个操作数都有元表时的函数获取规则:

    1. 对于二元操作符,如果第一个操作数有元表,并且元表中有所需要的字段定义,那么lua就以这个字段为元方法。
    2. 对于二元操作符,如果第一个操作数有元表,但是元表中没有所需要的字段定义,那么lua就去查找第二个操作数的元表。
    3. 如果两个操作数都没有元表,或者都没有对应的元方法定义,lua就会引发一个错误。
  • 保护元表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Set.new(l)
    local set = {}
    setmetatable(set,mt)
    for _, v in pairs(l) do set[v] = true end
    mt.__metatable = "You cannot get the metatable"
    return set
    end
    local tb = Set.new({1,2})
    print(tb) -- 输出:{1, 2}
    print(getmetatable(tb)) -- 输出__metatable的内容:You cannot get the metatable
    setmetatable(tb,{}) -- 输出错误信息:lua: test.lua:56: cannot change a protected metatable
  • 当访问一个table中不存在的字段时,lua的处理规则:

    1. 当table有这个字段时,直接返回对应的值;
    2. 当table没有这个字段时,解释器会去查找一个叫__index的元方法,并调用对应的元方法,返回结果
    3. 如果没有这个元方法,返回nil
      __index使用的例子:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      -- 假设要创建一些描述窗口,每个table中都必须描述一些窗口参数,例如颜色,位置和大小等
      Windows = {} -- 创建一个命名空间
      -- 创建默认值表
      Window.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}}
      Window.mt = {} --创建元表
      -- 声明构造函数
      function Windows.new(0)
      setmetatable(o, Window.mt)
      end
      -- 定义__lindex元方法
      Window.mt.__index = function (table, key)
      return Windows.default[key]
      end
      local win = Window.new({x = 10, y = 10})
      print(win.x) -- >10 访问自身已经拥有的值
      print(win.width) -- >100 访问default表中的值
      print(win.color.r) -- >255 访问default表中的值
      -- __index元方法不一定必须是个函数,它还可以是一个table。
    • newindex元方法,与index类似,newindex用于更新table中的数据,index用于查询table中的数据。当对table中不存在的索引赋值时,执行步骤如下:

      1. Lua解释器先判断这个table是否有元表,如果没有元表,就直接添加这个索引,然后对应赋值
      2. 如果有了元表,就查找元表中是否有__newindex元方法,如果有这个方法,lua解释器就执行这个元方法,而不是赋值
      3. 如果这个__newindex对应的不是一个函数,而是一个table时,lua解释器就在这个table中执行赋值,而不是在原来的table中
        示例代码:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        local tb1 = {}
        local tb2 = {}
        tb1.__newindex = tb2
        tb2.__newindex = tb1
        setmetatable(tb1,tb2)
        setmetatable(tb2,tb1)
        tb1.x = 10

      上面的代码存在循环引用的问题,lua解释器会抛出如下错误:

      1
      loop in settable
- rawget(tb,i); 直接访问tb属性,不使元表,即忽略__index的定义
- rawset(tb,k,v); 直接设置tb属性,不使用元表,即忽略__newindex的定义

代码示例

记录table的访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
local t = {} --原来的table
-- 保持对原table的一个引用
local _t = t
-- 创建代理
t = {}
-- 创建元表
local mt = {
__index = function(t, k)
print("access to element " .. tostring(k))
return _t[k]
end,
__newindex = function(t, k, v)
print("update of element " .. tostring(k))
_t[k] = v
end
}
setmetatable(t, mt)
t.x = 10 -- update of element x
print(t.x) -- access to element x

多个table访问日志,使用代理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 创建唯一索引
local index = {}
-- 创建元表
local mt = {
__index = function(t, k)
print("access to element " .. tostring(k))
return t[index][k]
end,
__newindex = function(t, k, v)
print("update of element " .. tostring(k))
t[index][k] = v
end
}
function track(t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
local t = {}
local proxy = track(t)
proxy.x = 10
print(proxy.x)

只读的table(代理模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
local tb = readOnly{1, 2, 3, 4, 5}
print(tb[1])
tb[1] = 20 -- Attempt to update a read-only table

面向对象

lua中,table就是对象,对象就是表,lua中的类本质上也是对象(表),不过是用来做模版的对象。
lua中的类和javascript中的非常类似,也是采用原型(prototype)的概念。lua通过元表实现原型。
像下面这样:

1
setmetatable(a, {__index = b})

lua中方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local Account = {balance = 0}
function Account.withDraw(self, v) -- 需要明确定义传入self参数
self.balance = self.balance - v
end
local a = Account
a.withDraw(a,10) -- 调用函数,需要明确指定self参数
-- 隐藏self参数的语法糖
function Account:withDraw(v) -- 注意,是":"号,且省略了self参数,但是函数仍然可以使用self
self.balance = self.balance - v
end
a = Account
a:withDraw(100) -- 注意这里的调用使用":"并且也不需要传入a本身
print(a.balance)

参考文章