常用的元表方法

409 阅读8分钟

元表

lua中每个值都可以有元表,

  • 默认情况下我们只可以为table类型的变量设置元表,其他类型需要通过c代码为某个类型设置元表。
  • string类型的值默认情况下共用一个元表,
  • 其他类型默认情况下没有元表(包括table类型)
print(getmetatable({})) --nil
print(getmetatable("44")) --table: 000002D826B11A20
print(getmetatable("454")) --table: 000002D826B11A20
print(getmetatable(print))--nil
print(getmetatable(4))--nil

__index

当一个表访问某个不存在的值时,一般情况下会返回nil。若该表有元表,就会访问其元表的__index元方法。

  • 若此元方法为表是,lua会从这个新表中查找该值。
--Set.lua
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__index = newTable

return Set

-- 调用测试
local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
print(tab[2],tab[4])  -- nil	4
setmetatable(tab,Set)
print(tab[2],tab[4]) --2	4
  • 若此方法为函数时,lua会调用该函数,函数的参数为原table和不存在的键k
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__index = function(tab,k)
    return "not contain  " .. k
end

return Set

-- 调用测试
local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
--print(tab[2],tab[4])
setmetatable(tab,Set) 
print(tab[2]) -- not contain  2
print(tab[4])  --4 

这个方法就常用来实现继承。可以使用rawget(t,i)来对表t进行原始的访问。此方法可以绕过元表,对表进行原始的访问。

local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
setmetatable(tab,Set)
print(rawget(tab,2)) --nil
print(tab[2]) -- not contain  2
print(tab[4]) --4

__newindex

当为表中不存在的某个键赋值时,就会调用该方法,

  • 此方法该函数时,其参数为原table,新k,新v
-- Set.lua
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__newindex = function(tab,k,v)
    print("not has this key")
end

Set.__index = newTable

return Set

-- 调用测试
local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
tab[2] = 2
print(tab[2]) -- 2
setmetatable(tab,Set)
tab[3] = 5 -- not has this key
print(tab[3]) -- 3

可以看到设置了元表后,当tab[3] = 5时,调用的时元表的__newindex方法,而调用print(tab[3])时调用元表的__index方法,输出了3。

我们可以使用rawset(t,k,v)为某个表设置新值,而不调用元表的__newindex方法。

---Set.lua
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__newindex = function(tab,k,v)
    rawset(tab,k,v)
end

Set.__index = newTable

return Set

---调用测试
local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
tab[2] = 2
print(tab[2])  --2
setmetatable(tab,Set)
tab[3] = 5
print(tab[3]) --5

这里看到当再次调用print(tab[3]) 的时候,输出的时5。是因为上面调用了rawset方法,为原表增加了新的值。 为什么不能在__newindex用下面的书写方式呢?

Set.__newindex = function(tab,k,v)
    tab[k] = v
end

是因为tab中原本不存在k,这样为表tab增加新值又会调用其元表的__newindex方法,从而导致无线循环。

  • 当__newindex是一个表是,新值会直接存到__newindex所对应的表中
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__newindex = newTable

Set.__index = function(tab,k)
    print("newTable start")
    for k,v in pairs(newTable) do
        print(k,v)
    end
    return "newTable end"
end

return Set
--- 
--- 调用测试
local Set = require("Set")
local tab = 
{
    [4] = 4,
    [5] = 5,
}
tab[2] = 2
print(tab[2]) -- 2
setmetatable(tab,Set)
tab[6] = 5
print(tab[6])

--输出结果
-- newTable start
-- 6	5
-- 1	1
-- 2	2
-- 3	3
-- newTable end

当访问原table中不存在的值tab[6]时,打印newTable的值时,发现多了键值为5的这一项。

__call方法

函数调用操作 func(args)。 当 Lua 尝试调用一个非函数的值的时候会触发这个事件 (即 func 不是一个函数)。 查找 func 的元方法, 如果找得到,就调用这个元方法, func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。

--- Set.lua
local Set = {}

local newTable = 
{
   [1] = 1,
   [2] = 2,
   [3] = 3,
}

Set.__newindex = newTable

Set.__index = function(tab,k)
   print("newTable start")
   for k,v in pairs(newTable) do
       print(k,v)
   end
   return "newTable end"
end

Set.__call = function(tab, ...)
   print(tab,...)
end

return Set

-- 调用测试
local Set = require("Set")
local tab = 
{
   [4] = 4,
   [5] = 5,
}
tab[2] = 2
setmetatable(tab,Set)
tab(2,45,4)
-- table: 000001D53F0CD5B0	2	45	4

__add

两个表直接使用+运算符时错,会调用其元表的__add元方法,因为默认情况下表没有元表,所以直接将两个表相加时会报错。

  • 当两个表相加时,若第一个表有元表,且有__add元方法,则调用第一个表的__add元方法,否则调用第二个的,若两个都没有,则会报错。
local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__newindex = newTable

Set.__index = function(tab,k)
    print("newTable start")
    for k,v in pairs(newTable) do
        print(k,v)
    end
    return "newTable end"
end

Set.__call = function(tab, ...)
    print(tab,...)
end

Set.__add = function(tab1,tab2)
    local sum = 0
    for _,v in pairs(tab1) do
        sum = sum + v
    end
    for _,v in pairs(tab2) do
        sum = sum + v
    end
    return sum
end

return Set

调用

local Set = require("Set")

local tab = 
{
    [4] = 4,
    [5] = 5,
}
setmetatable(tab,Set)
print(tab + tab)
--18

当然table也可以与number类型的值相加,只要保证table有元方法__add即可。

  • 与之类似的有__sub(减法),__mod(取模),__div(除法)等等。
  • 还有关系运算符,__eq(等于),__lt(小于),__le(小于等于),只有这三个,其他的例如~=可以转换为not(==),所以lua没有提供相应的机制。

__pairs

当时使用pairs方法遍历table时调用,若没有则,则默认放回next迭代函数,tab不可变量,nil

local Set = {}

local newTable = 
{
    [1] = 1,
    [2] = 2,
    [3] = 3,
}

Set.__newindex = newTable

Set.__index = function(tab,k)
    print("newTable start")
    for k,v in pairs(newTable) do
        print(k,v)
    end
    return "newTable end"
end

Set.__call = function(tab, ...)
    print(tab,...)
end

Set.__add = function(tab1,tab2)
    local sum = 0
    for _,v in pairs(tab1) do
        sum = sum + v
    end
    for _,v in pairs(tab2) do
        sum = sum + v
    end
    return sum
end

local function temp(tab,index)
    local nk, nv = next(tab, index)
    if nk then 
        nv = tab[2]
    end
    return nk, nv
end

Set.__pairs = function(tab)
    return temp,tab,nil
end

return Set

调用

local Set = require("Set")
local tab = 
{
    [1] = 4,
    [2] = 5,
}
setmetatable(tab,Set)
for k,v in pairs(tab) do
    print(k,v)
end
--输出
-- 2  5
·· 1  5

因为我们重写了__pairs元方法,返回的迭代函数temp中,返回值永远都是tab[2]的值。

lua垃圾收集

lua使用的时自动管理内存,但以提供了一些辅助垃圾收集的机制

  • 弱引用表允许lua释放还会被其他程序访问的表,类似于C#及其他语言中的弱引用机制。
  • 析构器可以帮助lua释放lua收集器不能直接控制的外部资源
  • collectgrabage允许我们设置垃圾收集其的步长,可以使垃圾收集机制更加高效

__mode

  • 弱引用表就是元素均为弱引用的表(分为三类:值为弱引用的,键值为弱引用的表,值和键都为弱引用的,无论那种类型的弱引用表,只要有一个键或值被回收了,那么整个键值对都会从表中删除),若一个对象只被若引用表持有,则lua最终会回收这个对象。
  • 一个表是否是弱引用,由其元表的__mode字段决定,__mode = "k",则表明键是弱引用的,__mode = "v"时表明值是弱引用的,__mode = "kv"变为值和键都是弱引用的。若不设置,说明表的键和值都是强引用,即使某个对象只最为了该表的键或值,也不会被回收。
local tab = {}
setmetatable(tab,{__mode = "k"})
local key1 = {}
local key2 = {}
tab[key1] = 1
tab[key2] = 2
key1 = nil
print("垃圾回收前")
for k,v in pairs(tab) do
    print(k,v)
end
collectgarbage()
print("垃圾回收后")
for k,v in pairs(tab) do
    print(k,v)
end

输出:

-- 垃圾回收前
-- table: 0000022E12E6E980	1
-- table: 0000022E12E6F240	2
-- 垃圾回收后
-- table: 0000022E12E6F240	2

可以看到,首先创建了两个table对象t1,t2,分别被key1,和key2持有,然后t1和t2有作为了键为弱引用的弱引用表tab的键值,此时t1分别被key1和tab持有,t2被key2和tab持有。当key1 = nil后,key1只被tab持有,当调用垃圾回收函数时,t1被回收了。验证了上面的说法。

  • 此字段可以用缓存机制,比如记忆函数,如访问某种颜色对象,若该类型颜色不存在,则创建,并缓存下来。随着访问的颜色种类的增多,缓存数据会越来越大,导致程序运行缓慢,这时候可以使用该机制,这样在垃圾回收的时候,回收缓存种没有被使用的对象。

__gc

析构器,类似于C#种的析构函,在垃圾回收期间会调用该对象的析构器。

local tab = {}
tab = setmetatable(tab,{__gc = function() print("gc") end})
collectgarbage()
-- 输出结果
-- gc

垃圾收集步骤

  • 标记,将可达的对象标记为活跃的
  • 清理,查找所以需要进行析构,但没有标记为活跃的对象,将其标记为或与状态,并放在一个单独的列表中,等待析构。同时清理弱引用表中未被标记的对象(也就是该对象只被弱引用表持有)
  • 清除,清除那些不可达的对象
  • 析构,调用那些需要被析构对象的析构器,下一次这些对象会被回收
  • 所以需要进行两次垃圾回收,才能删除需要析构的对象(第一次时调用析构器,第二次才是真正的回收)

常用的元表方法

__add(a, b)                     对应表达式 a + b
__sub(a, b)                     对应表达式 a - b
__mul(a, b)                     对应表达式 a * b
__div(a, b)                     对应表达式 a / b
__mod(a, b)                     对应表达式 a % b
__pow(a, b)                     对应表达式 a ^ b
__unm(a)                        对应表达式 -a
__concat(a, b)                  对应表达式 a .. b
__len(a)                        对应表达式 #a
__eq(a, b)                      对应表达式 a == b
__lt(a, b)                      对应表达式 a < b
__le(a, b)                      对应表达式 a <= b
__index(a, b)                   对应表达式 a.b
__newindex(a, b, c)             对应表达式 a.b = c
__call(a, ...)                  对应表达式 a(...)
__pairs(tab)                    泛型for时使用
__mode                          设置弱引用表
__gc(tab)                       析构器