元表

  • 元表处理Lua中的不可预见操作的table,如:两个table相加,对函数作比较,调用字符串。

    • 元表定义是实例行为,只能给出预先定义的操作集合的行为,不支持继承
    • 每一个值都可以有元表
      • table和userdata类型有独立的元表
      • 其他类型共享对应类型所属的同一个元表(必须通过C代码或调试库)
      • 在创建时不带元表
    • 可以使用setmetatable(table,metatable)设置元表,getmetatable()取得元表
    • 所有字符串在创建时都设置了同一个元表

      1. t = {}
      2. print(getmetatable("str1"))
      3. print(getmetatable("str2"))
      4. print(getmetatable(1))
      5. print(getmetatable(t))
    • 注:一个table可以成为任意值的元表;一组相关的table也可共享描述他们共同行为的元表;table还可成为自己的元表。

      元方法

  • 元方法元表中修改一个值在遇到未知操作时的行为,(是一个函数

    • 根据使用元表的原因,直接使用元方法
    • 假设a和b都是table,遇到a+b操作,过程如下:
      1. Lua检查a和b是否有元表
      2. 元表中是否有__add字段
      3. 调用该字段对应的值

        setmetatable()和getmetatable()

        setmetatable()

  • setmetatable(table,metatable):给table设置元表metatable

    • table:要设置元表的table,如果table的__metatable字段不为空,则抛出异常
    • metatable:元表,如果为nil清空table的元表
    • 要设置table类型外的其他类型的元表,只能使用C语言设置
    • 返回table

      getmetatable()

  • getmetatable(table):查询table是否有元表有则返回没有则返回__metatable关联数值

    算数运算相关的元方法

  • 每种算数运算符都有一个对应的元方法 | 算数运算符 | 元方法 |
    | 算数运算符 | 元方法 | | —- | —- | —- | —- | —- | | + | add | | &(按位与) | band | | - | sub | | |(按位或) | bor | | * | mul | | ~(按位异或) | bxor | | / | div | | ~(按位取反) | bnot | | // | idiv | | <<(逻辑左移) | shl | | -(负数) | unm | | >>(逻辑右移) | shr | | % | mod | | 连接运算符 | concat | | ^(幂运算) | __pow | | | |

Lua查找元方法步骤

  1. 如果第一个值有元表,且元表中存在所需元方法
    1. 使用这个元方法,与第二个值无关
    2. 结束
  2. 如果第二个值有元表,且元表中存在所需元方法
    1. 使用这个元方法
    2. 结束
  3. Lua抛出异常

    示例:集合并和交集运算

    ```lua local Set = {} local mt = {} —作为元表

function Set.new(t) —将table变为集合,并设置元表 local set = {} setmetatable(set, mt) —设置输入table的元表为mt for k, v in pairs(t) do —集和化table set[v] = true end return set end

function Set.tostring(set) —将集合字符化 local t = {} for k, v in pairs(set) do t[#t + 1] = k end return “{“ .. table.concat(t, “,”) .. “}” end

—[[ 开始设置元表的元方法 —]] mt.add = function(a, b) —设置add元方法,为求并集 local res = Set.new {}

for k in pairs(a) do    --加入集合a的元素
    res[k] = true
end

for k in pairs(b) do    --加入结合b的元素
    res[k] = true
end
return res

end

mt.mul = function(a, b) —设置mul元方法,为求交集 local res = Set.new {}

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

end

local s1 = Set.new {10, 20, 30, 50} —创建集合s1,s2并为其设置元表 local s2 = Set.new {30, 1}

local s3 = s1 + s2 —table s1和s2遇到不可预测的操作,对应元方法为求并集 print(Set.tostring(s3)) —输出:{1,10,20,30,50} s3 = nil s3 = (s1 + s2) * s1 print(Set.tostring(s3)) —输出:{10,20,30,50}


<a name="o70FN"></a>
# 关系运算相关的元方法

- 只有三种关系运算符可以指定其元方法,**其他三个关系运算符没有独立的元方法**
   - **注:如果两个对象类型不同,则不会调用其元方法,直接返回false**
   - Lua会对**没有独立元方法**的关系运算符进行**转换**:
      - a ~= b    →    not ( a == b )
      - a > b    →    b    <    a
      - a >= b    →    b    <=    a
| 关系运算符 | 元方法 |
| --- | --- |
| ==(等于) | __eq |
| <(小于) | __lt |
| <=(小于等于) | __le |

<a name="w0mwG"></a>
### 部分有序问题

- **并非所有类型**的元素都能够**正确排序**
   - 如:Not a Number(NaN)是不可以完全排序的
      - 规定上,涉及NaN的比较都返回false(NaN<=x为false,x<NaN为false)
<a name="QnW2L"></a>
### 示例:集合包含问题
```lua
local Set = {}
local mt = {} --作为元表

function Set.new(t) --将table变为集合,并设置元表
    local set = {}
    setmetatable(set, mt) --设置输入table的元表为mt
    for k, v in pairs(t) do --集和化table
        set[v] = true
    end
    return set
end

mt.__le = function(a, b)            --设置<=对应的元方法,求a是否是b的真子集
    for k, v in pairs(a) do
        if not b[k] then            --如果b中不含a则返回false
            return false
        end
    end
    return true
end

mt.__lt = function(a, b)                        --设置<对应的元方法,求a是否是b的子集
    return a <= b and not (b <= a)    --利用a是b的真子集 且 b不是a的真子集 来排除2个集合相等的情况
end

mt.__eq = function(a, b)            --设置==对应的元方法
    return a <= b and b <= a    --利用a是b的真子集,b又是a的真子集,来确定a和b相等
end

local s1 = Set.new {2, 4}
local s2 = Set.new {4, 10, 2}
print(s1 <= s2)            --输出:true
print(s1 < s2)            --输出:true
print(s1 > s1)            --输出:false
  • 注:上方程序中, 全部使用<=来实现,使得避免了因为部分有序而出现的问题

库定义相关的元方法

  • Lua检测一个操作中涉及是否存在对应元方法的元表
    • 如:库函数元表中定义和使用他们自己的字段,例:tostring函数

      tostring函数对应的元方法

  1. 使用函数tostring进行格式化
  2. 函数tostring会先检查元方法__tostring
  3. 元方法存在,则tostring函数调用该元方法
  4. tostring函数参数传递元方法
  5. 元方法返回值作为tostring函数返回值 ```lua local Set = {} local mt = {} —作为元表

function Set.new(t) —将table变为集合,并设置元表 local set = {} setmetatable(set, mt) —设置输入table的元表为mt for k, v in pairs(t) do —集和化table set[v] = true end return set end

mt.__tostring = function(set) local t = {} for k, v in pairs(set) do t[#t + 1] = k end return “{“ .. table.concat(t, “,”) .. “}” end

local s1 = Set.new {2, 4} print(s1)

<a name="KsTxh"></a>
### __metatable字段:保护元表

- **给元表设置__metatable**字段并**添加给table后**,用户既不能看到也**不能修改table的元表**
   - **设置__metatable**字段,并将元表**添加**给**table后**:
      - 对table调用函数**getmetatable**,会**返回该字段**
      - 对table调用函数**setmetatable**,会**引发错误**
```lua
local t = {}
local mt = {}
local mt2 = {}

mt.__metatable = "mot your business" --设置元表的__metatable字段,保护元表
setmetatable(t, mt)     --给table t设置元表mt

print(getmetatable(t)) --输出:__metatable内容,mot your business
setmetatable(t, mt2)     --报错

__pairs元方法

  • Lua5.2后添加了pairs的元方法,当一个对象有__pair元方法时,pairs函数会调用该元方法来完成遍历
    • __pairs元方法输入参数只能为一个(输入参数与pairs函数相同) ```lua local t = {} local mt = {}

mt.pairs = function(t) —设置pairs元方法 local i = 0 return function() —闭包 i = i + 1 if t[i] == nil then return nil else return t[i] end end end

t = {10, 20, 30, 40} setmetatable(t, mt)

for v in pairs(t), 0 do —使用pairs时调用的是__pairs元方法 print(v) end —[[ 输出: 10 20 30 40 —]]

<a name="JeG58"></a>
# 
<a name="MTYrh"></a>
# table相关的元方法
<a name="MQn9w"></a>
### __index元方法:访问时调用

- **访问**一个**table**中**不存在**的字段会得到nil
   - 实际的**Lua解释器**操作:
      1. Lua解释器**查找**名为**__index**的**元方法**
      1. **没有**,**返回nil**
      1. **有**,由这个**元方法提供**最终**结果(设置为table本身,就是直接访问table)**
- 使用元方法**__index可以实现继承**
   - **函数作**__index**元方法**可实现**单继承**,该方法的**开销更高**,**但是灵活**
- **元方法可以是函数,或是table**
   - __index元方法是**函数**时:会以table和**不存在的键作为参数调用**该函数
   - __index元方法是**table**时:**访问**该table
```lua
local prototpe = {x = 0, y = 0, width = 100, height = 100}    --作为父类
local mt = {}

function new(t)                    
    setmetatable(t, mt)                    --设置元表
    return t
end

mt.__index = function(t, key)        --设置__index元方法
    return prototpe[key]
end

local w = new {x = 10, y = 20}    --实例化新地类w
print(w.width)                                    --输出:100
--[[
table w中并没有width说对应的数值,但是因为调用w.width时,w中没有width字段,
会使用__index元方法,通过元方法调用了父类中width,因此输出了父类
--]]

rawget():不调用元方法访问表

  • rawget(table,index)不调用__index元方法,访问table

    • table:要访问的table
    • index:table中的键

      __newindex元方法:更新时调用

  • 与__index类似,用于table的更新(table中键的赋值

    • Lua解释器的使用过程
      1. table不存在的索引进行赋值
      2. 查找__newindex元方法是否存在
      3. 存在
        • 函数调用执行赋值
        • table:在table中执行赋值
      4. 不存在,在原table中进行赋值 ```lua local t = {x=1} local mt = {}

mt.newindex = function(t,k,v) —设置newindex元方法为函数,在对新索引赋值时,会调用该函数 print(“hello”) end

setmetatable(t, mt) —设置元表 t.x = 2 —成功赋值 t.a = 1 —a不在table t中,调用元方法__newindex,输出:hello


local t = {x = 1} local mt = {} local save = {}

mt.newindex = save —设置newindex元方法为table,对新索引赋值时,就会在该table中进行 setmetatable(t, mt) —设置元表

t.a = 3 —对新索引a赋值 print(t.a) —输出:nil print(save.a) —输出:3

<a name="aMOGv"></a>
#### rawset():不调用元方法赋值

- **rawset(table,key,value)**:**在table中的索引key赋值时,绕过元方法__newindex**
   - table:要操作的table
   - key:table中的索引
   - value:table中索引对应的值

<a name="cze7J"></a>
### 具有默认值的table
```lua
function setDefault(t, d)
    local mt = {
        __index = function()
            return d
        end
    }
    setmetatable(t, mt)
end

local t = {x = 10, y = 20}
print(t.z)                --输出:nil
setDefault(t, 0)    --设置索引的初始值为0
print(t.z)                --输出:0
  • 上方程序中,函数创建了一个闭包新的元表是如果有很多表需要设定初始值,会照成比较大开销
    • 因为每个table都是和不同的元表连接在一起的(也就是不同的闭包函数)。
    • 因此为了减少开销,需要让多个table可以使用同一个元表 ```lua local mt = { —将元表的定义放在函数之外 index = function(t, k) return t.only —元方法index返回的是参数table中的键only,该键在调用函数时设定 end }

function setDefault(t, d) t.only = d setmetatable(t, mt) end

local t = {x = 10, y = 20} print(t.z) —输出:nil setDefault(t, 0) —设置索引的初始值为0 print(t.z) —输出:0


- 上方程序中,将**初始值保存在**了**table中**,这样就可以**让多个table使用同一个元表**,减少开销

<a name="TjSXB"></a>
### 跟踪对table的访问

- 监控一个table的所有访问,**创建一个代理**
   - **捕获**一个table的**所有访问**的唯一方式是**保持table是空的**
      - 因为**__index**和**__newindex**只有在**索引不存在**时,才有用
```lua
function track(t)
    local proxy = {}        --作为table t的代理table

    local mt = {                    --创建代理的元表
        __index = function(t, k)    
            print("*aaccess to element " .. tostring(k))
            return t[k]        --访问原来的table
        end,

        __newindex = function(t, k, v)
            print("*uptate of element " .. tostring(k) .. "to" .. tostring(v))
            t[k] = v            --更新原来的table
        end,

        __pairs = function()
            return function(t, k)    --迭代函数
                local nextKey, nextValue = next(t, k)
                if nextKey ~= nil then
                    print("*traversing element " .. tostring(nextKey))
                end
                return nextKey, nextValue
            end
        end,

        __len = function()
            print("*check table long")
            return #t
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

t = {}
t = track(t)        --给table t加一个代理,用于最终操作
t[1] = "hello"    
print(t[1])
--[[
输出:
*uptate of element 1 to hello
*aaccess to element 1
hello
--]]
  • 上方的程序中,根据设计的规则跟踪每个访问,并将其重定向到原来的table中
    • 对代理table来说,因为其是空table,所以所有的索引都是未知
    • 如果需要同时监控几个table,可以像给table设置默认值一样,用一个共享的元表减少开销

只读table

  • 使用代理的概念,跟踪对table更新操作抛出异常
    • __index直接使来的table来替代(更快也更简单) ```lua function readOnly(t) local proxy = {} local mt = { index = t, newindex = function()
         error("attempt to upadte a read-only table")
      
      end } setmetatable(proxy, mt) return proxy end

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

print(days[1]) —输出:Sunday days[2] = “null” —输出:attempt to upadte a read-only table ```