lua 环境下基于 metatable 的 table 行为总结

tablelua 的重要数据类型,lua 本身的很多特性都与 table 相关,如 lua 运行时的环境参数、luamodulepackage 等。借助 metatable*,可使 *lua 具备面向对象的部分特性,提供诸如 class/objectinheritance 等功能,使 lua 能够胜任更多的应用场合。

  • table 基础

    lua table 有两种表现形式,一是数组形式,二是 (key, value) 键值对形式,如下所示
    -- 数组形式的 table: t1
    t1 = {}
    t1[1] = 1
    t1[2] = 2
    t1[3] = 3
    -- 键值对形式的 table: t2
    t2 = {}
    t2["k1"] = "hello"
    t2["k2"] = "world"
    键值对 (key, value) 中,key 可以是任意 lua 数据类型,因此数组形式的 table 可理解为该 tablekey 是一连串从1开始的连续的整型数字。(key, value)table 一般表现形式。
    虽然 key 可以是任意 lua 数据类型,但针对不同数据类型的 keylua 环境提供了一些操作特性。
    key 是从 1 开始的连续整型数字时,#t1 可获取 t1 内成员的数量,否则 #t1 的值为 0。
    key 是字符串时,可通过点 . 操作符来获取或设置该键所对应的值,如下所示
    t2 = {}
    t2["k1"] = "hello"
    t2.k2 = "world"
    print(t2.k1, t2.k2)
    最后,不要忽略 key 可以是任意 lua 数据类型的特性。在程序设计时,可能需要某些 object 作为 key
  • tablemetatable 的成员

    metatable 也是普通的 lua table*,当 *metatable 设置了特定的成员后,它将决定某些 table 的行为,前提是这些 table 设置了该 metatable
    默认情况下,
    table
    metatablenil*,获取/设置 *metatable 的方法如下所示
    t = {}
    m = {}
    getmetatable(t) -- return nil
    setmetatable(t, m) -- set m as t's metatable
    metatable 的成员被称为 metamethod*,在操作 *table 时,lua 会检查对应的 metatable 以及该 metatable 所包含的 metamethod*,当条件满足时,调用 *metamethod 完成相应操作。
    按功能划分,metamethod 可分为三类,一类负责运算符重定义,一类负责系统库函数重定义,一类负责 table 成员的访问。
    • metamethod 分类一:运算符重定义

      此类 metathod 类似于 c++ 的运算符重载,使用加减乘除等运算符来替代函数调用,简化代码编写。
      可重定义的运算符包括算术运算符,如数字运算:加、减、乘、除,位运算:按位取反、按位与、左移等;还包括逻辑运算,如大于、等于、小于、不等于等。
      示例代码如下所示:
      m = {}
      -- 重定义逻辑运算:< (小于)
      m.__lt = function(a, b)
      return a.v < b.v
      end
      t1 = { v = 20 }
      t2 = { v = 5 }
      setmetatable(t1, m)
      setmetatable(t2, m)
      res = t1 < t1 -- res 值为 false
    • metamethod 分类二:系统库重定义

      目前 lua 提供三个系统库的重定义,分别是 __tostring__pairs__metatable
      __tostring:如果 print(t)tmetatable 定义了 __tostring,则 print 函数将自动调用 __tostring
      __pairs:如果 for k, v in pairs(t) dotmetatable 定义了 __pairs,则 pairs 函数自动调用 __pairs
      __metatable:供 getmetatablesetmetatable 函数使用。调用 getmetatable(t) 函数,如果 tmetatable 定义了 __metatable,则返回 __metatableprint 结果。调用 setmetatable(t, m) 函数,如果 tmetatable 定义了 __metatable,则返回报错信息,提示 tmetatable 不允许修改。
      需要注意的是,__pairs__tostring 必须定义成 function 类型,而 __metatable 可以是任意 lua 数据类型。
    • metamethod 分类三:table 成员访问

    lua 使用 metatable__index__newindex 控制 table 的成员访问。
    __index 负责 table 成员读,如 print(t.a) 将读取 table t 的成员 a
    __newindex 负责 table 成员的写,如 t.a = 3table t 的成员 a 设置为 3。注,lua 使用赋值来创建和删除 table 成员,如 t.a = 3 时,如果 t.a 不存在,则 lua 自动创建 t.a 并赋值;又如 t.a = nil,则 lua 自动删除 t.a 成员。
    虽然称 __index__newindexmetamethod*,但是 __index__newindex 可以是 *function 类型,也可以是 table 类型。
  • table 成员访问行为

    table 成员访问行为分为 table 成员的读、写和行为传导。
    • table 成员

      tablemetatablenilmetatable__index 成员为 nil 时,将按照 table 成员的实际情况访问 table 的成员。如:
      t = {}
      t.a = "a" -- 写
      print(t.a, t.b) -- 读,t.a = "a", t.b = nil
      tablemetatable__index 值为 function 时,并且读取的 table 成员不存在时,lua 将自动调用 __index 所指向的函数。如:
      t1 = {}
      t1.a = 5
      m = {}
      m.__index = function(t, k)
      print(t, k)
      end
      setmetatable(t1, m)
      print(t1.a) -- 读,t1.a 存在,不触发 __index,t1.a == 5
      print(t1.b) -- 读,t1.b 不存在,将调用 m.__index 所指向的函数
      注,__index 指向函数的声明形式为 function(t, k),其中,t 为设置了该 metatabletablek 为所需访问该 table 成员的名称,或理解为 table (key, value) 键值对中的 key*,可通过 t[k] 引用。
      当 *table t1
      metatable__index 值为另一 table(设为 *x) 时,并且读取的 *t1 成员 t1.b 不存在时,lua 将自动查找 x 对应成员名称,并返回之,即 x.b。如:
      -- metatable settings
      x = {}
      x.b = "hello"
      m = {}
      m.__index = x
      -- table settings
      t1 = {}
      t1.a = 5
      setmetatable(t1, m)
      print(t1.a) -- 读,t1.a 存在,不触发 __index,t1.a == 5
      print(t1.b) -- 读, t1.b 不存在,return value == x.b == "hello"
      print(t1.c) -- 读, t1.c 不存在,return value == x.c == nil
      for k, v in pairs(t1) do
      print(k, v)
      end
      -- result: a 5
      rawget 函数的使用。当读取 table t1 成员 t1.a 时,无论采取 t1.at1["a"] 形式,都会触发 metatable__index 机制。有些场合不需要该机制(或者说不能使用该机制),此时 rawget 将派上用场。rawget(t1, "a") 将获取 t1.a 但不触发 __index。如:
      m = {}
      m.__index = function(t, k)
      return t[k] -- 此方式触发 t 的 metatable 的 __index 机制,会出现死循环
      end
      t1 = {}
      setmetatable(t1, m)
      print(t1.a) -- stdin:2: C stack overflow
      正确做法是:
      m = {}
      m.__index = function(t, k)
      return rawget(t, k) -- rawget 单纯读取 t1.a,不会触发 t 的 metatable 的 __index 机制
      end
      t1 = {}
      setmetatable(t1, m)
      print(t1.a)
    • table 成员

      tablemetatablenilmetatable__newindex 成员为 nil 时,将按照 table 成员的实际情况访问 table 的成员。如:
      t = {}
      t.a = "a" -- 写
      print(t.a, t.b) -- 读,t.a = "a", t.b = nil
      tablemetatable__newindex 值为 function 时,并且访问的 table 成员不存在时,lua 将自动调用 __newindex 所指向的函数。如:
      t1 = {}
      t1.a = 5 -- 写,在设置 metatable 前 t1 的 metatable 为 nil,此时写不会触发 __newindex
      m = {}
      m.__newindex = function(t, k, v)
      print(t, k, v) -- 打印 t, k, v,并未做实际写,此时 t["k"] 值为 nil
      end
      setmetatable(t1, m)
      t1.a = 10 -- 写,t1.a 存在,不触发 __newindex,t1.a == 10
      t1.b = "hello" -- 写,t1.b 不存在,将调用 m.__newindex 所指向的函数
      注,__newindex 指向函数的声明形式为 function(t, k, v),其中,t 为设置了该 metatabletablek 为所需访问该 table 成员的名称,v 为所需设置的值,或理解为 table (key, value) 键值对中的 keyvalue*。
      当 *table t1
      metatable__newindex 值为另一 table(设为 *x) 时,并且写入的 *t1 成员 t1.b 不存在时,lua 将自动查找 x 对应成员名称,并写入之,即 x.b=?。如:
      -- metatable settings
      x = {}
      x.b = "hello"
      m = {}
      m.__newindex = x
      -- table settings
      t1 = {}
      t1.a = 5 -- 写,在设置 metatable 前 t1 的 metatable 为 nil,此时写不会触发 __newindex
      setmetatable(t1, m)
      t1.a = 10 -- 写,t1.a 存在,不触发 __newindex,t1.a == 10
      t1.b = "HELLO" -- 写, t1.b 不存在,写入x,x.b == "HELLO"
      t1.c = "world" -- 写,t1.c 不存在,写入x,x.c = "world"
      print(t1.c) -- 读,return value == x.c == nil, t1.c 不存在
      for k, v in pairs(t1) do
      print(k, v)
      end
      -- result: a 10
      for k, v in paris(x) do
      print(k, v)
      end
      -- result: b HELLO
      -- c world
      rawset 函数的使用。当写入 table t1 成员 t1.a 时,无论采取 t1.a=?t1["a"]=? 形式,都会触发 metatable__newindex 机制。有些场合不需要该机制(或者说不能使用该机制),此时 rawset 将派上用场。rawset(t1, "a", ?) 将写入 t1.a=? 但不触发 __newindex。如:
      m = {}
      m.__newindex = function(t, k, v)
      t[k] = v -- 此方式触发 t 的 metatable 的 __newindex 机制,会出现死循环
      end
      t1 = {}
      setmetatable(t1, m)
      t1.a = 5 -- stdin:2: C stack overflow
      正确做法是:
      m = {}
      m.__newindex = function(t, k, v)
      rawset(t, k, v) -- rawset 单纯写入 t1.a,不会触发 t 的 metatable 的 __newindex 机制
      end
      t1 = {}
      setmetatable(t1, m)
      t1.a = 5
    • 行为传导

      __index__newindexluatable 类型时,__index__newindex 也能够设置 metatable*,在这种情况下,会出现__index__newindex 机制的传导现象。在 *lua 5.3 下做如下实验:
      读,链式传导
      -- metatable1 settings
      x1 = {}
      x1.b = "b"
      m1 = {}
      m1.__index = x1
      -- metatable2 settings
      x2 = {}
      x2.c = "c"
      m2 = {}
      m2.__index = x2
      -- create link
      setmetatable(x1, m2)
      -- table settings
      t1 = {}
      t1.a = "a"
      setmetatable(t1, m1)
      print(t1.a, t1.b, t1.c, t1.d) -- result: a b c nil
      for k, v in pairs(t1) do
      print(k, v)
      end
      -- result: a a
      -- 读,链式传导,正常,传导至最末端
      读,环型传导
      -- metatable settings
      x = {}
      x.b = "b"
      m = {}
      m.__index = x
      -- create ring
      setmetatable(x, m)
      -- table settings
      t = {}
      t.a = "a"
      setmetatable(t, m)
      print(t.a, t.b)
      -- result: a b
      -- 读取存在的 key,正常
      print(t.c)
      -- result: 报错,stdin:1: '__index' chain too long ...
      -- 读取不存在的 key,导致死循环
      写,链式传导
      -- metatable1 settings
      x1 = {}
      x1.b = "b"
      m1 = {}
      m1.__newindex = x1
      -- metatable2 settings
      x2 = {}
      x2.c = "c"
      m2 = {}
      m2.__newindex = x2
      -- create link
      setmetatable(x1, m2)
      -- table settings
      t1 = {}
      t1.a = "a"
      setmetatable(t1, m1)
      t1.a = "aa"
      t1.b = "bb"
      t1.c = "cc"
      t1.d = "dd"
      for k, v in pairs(t1) do
      print(k, v)
      end
      -- result: a aa
      for k, v in pairs(x1) do
      print(k, v)
      end
      -- result: b bb
      for k, v in pairs(x2) do
      print(k, v)
      end
      -- result: c cc
      -- d dd
      -- 写,链式传导,正常,传导至最末端
      写,环型传导
      -- metatable settings
      x = {}
      x.b = "b"
      m = {}
      m.__newindex = x
      -- create ring
      setmetatable(x, m)
      -- table settings
      t = {}
      t.a = "a"
      setmetatable(t, m)
      t.a = "aa"
      t.b = "bb"
      for k, v in pairs(t) do
      print(k, v)
      end
      -- result: a aa
      for k, v in pairs(x) do
      print(k, v)
      end
      -- result: b bb
      -- 写入存在的 key,正常
      t.c = "cc"
      -- 报错:stdin:1: '__index' chain too long ...
      -- 写入不存在的 key,导致死循环
      结论如下:
      metatable__index__newindex 作为 table 来使用时,metatable 行为在 __index__newindex 上也会出现,并层层传导至最末端。应尽量避免出现环型传导。
    • 面向对象基础(读,链式传导的紧凑形式)

      m1 = {}
      m1.b = "b"
      m1.__index = m1 -- 自身作为 __index
      m2 = {}
      m2.c = "c"
      m2.__index = m2 -- 自身作为 __index
      setmetatable(m2, m1)
      t = {}
      t.a = "a"
      setmetatable(t, m2)
      print(t.a, t.c, t.b)
      从面向对象的角度观察上述代码,t 继承了 m2 的成员,m2 继承了 m1 成员,实现了简单的继承特性。
  • 结束语

    注意 tablemetatablemetamethod 之间的关系:table 可设置 metatable, metatable 是普通的 table 数据类型,metamethodmetatable 的成员。
    可利用 metamethod*:__index__newindex 控制 *table 成员的访问,并实现面向对象特性。