Lua 学习之基础篇八<Lua 元表(Metatabble)&&继承>

讲到元表,先看一段table的合并动作.

t1 = {1,2}
t2 = {3,4}
t3 = t1 + t2
attempt to perform arithmetic on a table value (global 't1')

程序会报错,因为不知道如何对两个table执行+运算,这个时候就需要通过元表来定义,有点类似c中的运算符加载。我们看一下如何通过元表实现合并操作。


local mt = {}
--定义mt.__add元方法(其实就是元表中一个特殊的索引值)为将两个表的元素合并后返回一个新表
mt.__add = function(t1,t2)
    local temp = {}
    for _,v in pairs(t1) do
        table.insert(temp,v)
    end
    for _,v in pairs(t2) do
        table.insert(temp,v)
    end
    return temp
end
local t1 = {1,2,3}
local t2 = {2}
--设置t1的元表为mt
setmetatable(t1,mt)

local t3 = t1 + t2
--输出t3
local st = "{"
for _,v in pairs(t3) do
    st = st..v..", "
end
st = st.."}"
print(st)

{1, 2, 3, 2, }

可以看到, 程序在执行的时候,调用了mt._add元方法计算。

具体的过程是:
1.查看t1是否有元表,若有,则查看t1的元表是否有__add元方法,若有则调用。
2.查看t2是否有元表,若有,则查看t2的元表是否有__add元方法,若有则调用。
3.若都没有则会报错。
所以说,我们通过定义了t1元表的__add元方法,达到了让两个表通过+号来相加的效果

Lua 查找一个表元素时的规则,其实就是如下 3 个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
3.判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。

Lua提供两个用来处理元表的方法

  • setmetatable(table, metatable)为表设置元表metatable,不能从Lua中改变其它任何类型的值的元表metatable(使用debug库例外),要这样做的话必须使用C语言的API。

  • getmetatable(table)获取表的元表metatable对象

元表的元方法有:(下标是__双底线喔)

函数 描述
__add 运算符 +
__sub 运算符 -
__mul 运算符 *
__ div 运算符 /
__mod 运算符 %
__unm 运算符 -(取反)
__concat 运算符 ..
__eq 运算符 ==
__lt 运算符 <
__le 运算符 <=
__call 当函数调用
__tostring 转化为字符串
__index 调用一个索引
__newindex 给一个索引赋值

__index取下标操作用于访问`table[key]

__newindex赋值给指定下标`table[key]=value

__tostring转换成字符串

__call当Lua调用一个值时调用

__mode用于弱表`week table

__metatable用于保护metatable不被访问

  • __add

当Lua试图将两个表相加时,会首先检查两个表之一是否有元素,然后检查该元表中是否具有一个叫做__add的字段。如果Lua找到了该字段,则会调用该字段对应的值。这个值就是所谓的“元方法”。


local tbl1 = {1,2,3}
local tbl2 = {5,1,1}
-- print(#tbl1, #tbl2)

-- 无法直接相加两个表
-- printf(tbl1 + tbl2)

-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
    local result = {}
    local length = #x
    for i=1,length do
        result[i] = x[i] + y[i]
    end
    return result
end
-- test
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的相加操作
local result = tbl1  + tbl2
-- 循环输出
for k,v in ipairs(result) do
    print(k, v)
end
1	6
2	3
3	4
  • __concat 表连接
-- 表的连接
local tbl1 = {1,2,3}
local tbl2 = {2,3,4}

-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
    local result = {}
    local length = #x
    for i=1,length do
        result[i] = x[i] + y[i]
    end
    return result
end
-- 实现表的连接操作
mt.__concat = function(x, y)
    local length = #x
    local result = {}
    for i=1,length do
        result[i] = x[i].."**"..y[i]
    end
    return result
end
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的连接操作
local result = tbl1..tbl2
-- 循环输出
for k,v in ipairs(result) do
    print(k, v)
end

1	1**2
2	2**3
3	3**4

下面举一个例子,集合运算

使用表table表示集合,实现集合计算中的并集、交集等。大家看一下写法,体会一下元表的运用


-- 定义集合
Set = {}
-- 定义集合的元表
local mt = {}


-- 创建集合,根据参数列表值创建新集合
function Set.new(arr)
    local set = {}
    setmetatable(set, mt) --创建集合均具有一个相同的元表
    for i,v in ipairs(arr) do
        set[v] = true
    end
    return set
end
-- 求集合并集
function Set.union(x, y)
    local result = Set.new{}
    for k,v in pairs(x) do
        result[k] = true
    end
    for k,v in pairs(y) do
        result[k] = true
    end
    return result
end
-- 求集合交集
function Set.intersection(x, y)
    -- 创建空集合
    local result = Set.new{}
    for k in pairs(x) do
        result[k] = y[k]
    end
    return result
end
-- 将集合转换为字符串
function Set.tostring(set)
    -- 定义存放集合中所有元素的列表
    local result = {}
    for k in pairs(set) do
        result[#result + 1] = k
    end
    return "{"..table.concat(result, ", ").."}"
end
-- 打印输出集合元素
function Set.print(set)
    print(Set.tostring(set))
end

-- 设置集合元方法
mt.__add = Set.union

-- 测试
local set1 = Set.new{10,20,30,40}
local set2 = Set.new{30, 1,50}


Set.print(set1 + set2) -- {1, 40, 10, 20, 30, 50}
Set.print(Set.intersection(set1, set2)) -- {30}
  • __call

__call可以让table当做一个函数来使用。

local mt = {}
--__call的第一参数是表自己
mt.__call = function(mytable,...)
    --输出所有参数
    for _,v in ipairs{...} do
        print(v)
    end
end

t = {}
setmetatable(t,mt)
--将t当作一个函数调用
t(1,2,3)
1
2
3

再举一个例子,注意call 里面的参数调用

local mt = {}
sum = 0
--__call的第一参数是表自己
mt.__call = function(mytable,val)
    --输出所有参数
    for i = 1,#mytable do
        sum = sum +mytable[i]*val
    end
    return sum
end

t = {1,2,3}
setmetatable(t,mt)
--将t当作一个函数调用
print(t(5))
--30
  • __tostring

__tostring可以修改table转化为字符串的行为

local mt = {}
--参数是表自己
mt.__tostring = function(t)
    local s = "{"
    for i,v in ipairs(t) do
        if i > 1 then
            s = s..", "
        end
        s = s..v
    end
    s = s .."}"
    return s
end

t = {1,2,3}
--直接输出t
print(t)
--将t的元表设为mt
setmetatable(t,mt)
--输出t
print(t)
table: 0x7fcfe7c06a80
{1, 2, 3}
  • __index

调用table的一个不存在的索引时,会使用到元表的__index元方法,和前几个元方法不同,__index可以是一个函数也可是一个table。
作为函数:
将表和索引作为参数传入__index元方法,return一个返回值

local mt = {}
--第一个参数是表自己,第二个参数是调用的索引
mt.__index = function(t,key)
    return "it is "..key
end

t = {1,2,3}
--输出未定义的key索引,输出为nil
print(t.key)
setmetatable(t,mt)
--设置元表后输出未定义的key索引,调用元表的__index函数,返回"it is key"输出
print(t.key)
local tbl = {x=1, y=2}
-- table中字段默认值为nil
print(tbl.x, tbl.y, tbl.z) -- 1 2 nil
-- 通过metatable修改table的默认值
function setTableDefault(tbl, default)
    local mt = {
        __index = function()
            return default 
        end
    }
    setmetatable(tbl, mt)
end
-- 调用setTableDefault后,任何对tbl中存在的字段的访问都回调用它的__index
setTableDefault(tbl, 1)
print(tbl.x, tbl.y, tbl.z) -- 1 2 1

作为table:
查找__index元方法表,若有该索引,则返回该索引对应的值,否则返回nil

local mt = {}
mt.__index = {key = "it is key"}

t = {1,2,3}
--输出未定义的key索引,输出为nil
print(t.key)
setmetatable(t,mt)
--输出表中未定义,但元表的__index中定义的key索引时,输出__index中的key索引值"it is key"
print(t.key)
--输出表中未定义,但元表的__index中也未定义的值时,输出为nil
print(t.key2)
nil
it is key
nil
  • __newindex

__newindex__index类似,不同之处在于__newindex用于table的更新,__index用于table的查询;当为table中一个不存在的索引赋值时,会去调用元表中的__newindex元方法
1.作为函数
__newindex是一个函数时会将赋值语句中的表、索引、赋的值当作参数去调用。不对表进行改变

local mt = {}
--第一个参数时表自己,第二个参数是索引,第三个参数是赋的值
mt.__newindex = function(t,index,value)
    print("index is "..index)
    print("value is "..value)
end

t = {key = "it is key"}
setmetatable(t,mt)
--输出表中已有索引key的值
print(t.key)
--为表中不存在的newKey索引赋值,调用了元表的__newIndex元方法,输出了参数信息
t.newKey = 10
--表中的newKey索引值还是空,上面看着是一个赋值操作,其实只是调用了__newIndex元方法,并没有对t中的元素进行改动
print(t.newKey)
it is key
index is newKey
value is 10
nil


-- 定义原表
local mt = {}
mt.__index = function(tbl, key)
    return mt[key]
end
mt.__newindex = function(tbl, key, value)
    mt[key] = value
    print(string.format("modify: key=%s value=%s", key, value))
end

local window = {x=1}
setmetatable(window, mt)

print(window.x) -- 1
print(rawget(window, x)) -- nil

-- 添加属性
print ("-------------")
window.y = 2
print ("-------------")
for k,v in pairs(mt) do
    print (k,v)
end
print ("-------------")
for k  in pairs(mt) do
    print (k)
end
print ("-------------")
print(window.y) -- 2
print(rawget(window, y)) -- nil
1
nil
-------------
modify: key=y value=2
-------------
__index	function: 0x7fde254066f0
y	2
__newindex	function: 0x7fde25406b00
-------------
__index
y
__newindex
-------------
2
nil

2.作为table
__newindex是一个table时,为t中不存在的索引赋值会将该索引和值赋到__newindex所指向的表中,不对原来的表进行改变。

local mt = {}
--将__newindex元方法设置为一个空表newTable
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)

--对t中不存在的索引进行负值时,由于t的元表中的__newindex元方法指向了一个表,所以并没有对t中的索引进行赋值操作将,而是将__newindex所指向的newTable的newKey索引赋值为了"it is newKey"
t.newKey = "it is newKey"

print(t.newKey,newTable.newKey)
nil	nil
nil	it is newKey

当然如果主表中存在该索引,自然会直接赋值,不会传递元表中赋值。我们也可以直接改写newindex,用rawset直接赋值


Window = {}
Window.mt = {}

function Window.new(o)
	setmetatable(o ,Window.mt)
	return o
end
Window.mt.__index = function (t ,key)
	return 1000
end
Window.mt.__newindex = function (table ,key ,value)
	if key == "wangbin" then
		rawset(table ,"wangbin" ,"yes,i am")
	end
end
w = Window.new{x = 10 ,y = 20}
w.wangbin = "55"
print(w.wangbin)
yes,i am

rawget 和 rawset

有时候我们希望直接改动或获取表中的值时,就需要rawget和rawset方法了。
rawget可以让你直接获取到表中索引的实际值,而不通过元表的__index元方法。

rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效

local mt = {}
mt.__index = {key = "it is key"}
t = {}
setmetatable(t,mt)
print(t.key)
--通过rawget直接获取t中的key索引
print(rawget(t,"key"))
it is key
nil

rawset可以让你直接为表中索引的赋值,而不通过元表的__newindex元方法。

local mt = {}
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)
--通过rawset直接向t的newKey索引赋值
rawset(t,"newKey","it is newKey")
print(t.newKey,newTable.newKey)
nil	nil
it is newKey	nil
local mt = {}
t = {}
setmetatable(t,mt)
rawset(t,"newKey","it is newKey")
for k ,v in pairs (t) do
    print (k,v)
end

print(t.newKey)
newKey	it is newKey
it is newKey

下面举几个例子,讲述一下各个方法之间的关系。

local tb = {}
setmetatable(tb, { __index = function()
    return "not find"
end })
setmetatable(tb, { __newindex = function(table, key, value)
    local patchKey = "version"
    if key == patchKey then
        rawset(table, patchKey, "补丁值")
    else
        rawset(table, key, value)
    end
end })
-- setmetatable(tb, { __index = function()
--     return "not find"
-- end })
tb.version = "正常版本"
tb.date = "2018"
print(tb.version) --打印 补丁值
print(tb.server) --打印nil,不会调用__index方法了?
print(tb.date)  --打印2018

经过测试发现:

如果__index在__newindex之前,则不会调用__index

如果把_index放在__newindex之后,调用不存在值,才会调用__index方法

--谁在后面就会调用谁,前一个会失效。但是这个取决于你定于元方法的方式(我们一般定义元方法方式如下),看下面的写法没问题;


local tb = {}
local mt ={}
mt.__newindex = function(table, key, value)
    local patchKey = "version"
    if key == patchKey then
        rawset(table, patchKey, "补丁值")
    else
        rawset(table, key, value)
    end
end 

mt.__index = function()
    return "not find"
end
setmetatable(tb,mt)

tb.version = "正常版本"
tb.date = "2018"
print(tb.version) 
print(tb.server) 
print(tb.date)  
补丁值
not find
2018

rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效

--- Gets the real value of `table[index]`, the `__index` metamethod. `table`
--- must be a table; `index` may be any value.
---@param table table
---@param index any
---@return any
function rawget(table, index) end
local tb = {}
local mt ={mm = "test"}
mt.__index = function()
    return "not find"
end
setmetatable(tb,mt)

tb.version = "正常版本"
print(tb.version)
print(tb.server) ---不存在的值,调用__index方法
--rawget是为了绕过__index而出现的,直接点,就是让__index方法的重写无效
print(rawget(tb, "version")) --打印 正常版本
print(rawget(tb, "server")) --打印nil

利用元表的特性实现对象继承


local function class( super )
    local cls
    if super then
        cls = {}
        cls.super = super
        setmetatable(cls, {__index = super})
    else
        -- ctor是构造函数的命名
        cls = {ctor = function () end}
    end

    cls.__index = cls
    function cls.new( ... )
        local instance = setmetatable({}, cls)
        instance:ctor()
        return instance
    end

    return cls
end
--测试实现部分
local Test = class()
function Test:doSomething()
    print("test doSomething")
end
local test = Test.new()
test:doSomething()

--测试继承部分
local Test = class()
function Test:doSomething()
    print("test doSomething")
end
local Test2 = class(Test)
local test = Test2.new()
test:doSomething()

在new的时候,创建一个table并返回,即创建一个实例,实例可以有自己的字段,比如Test类的doSomething,该字段是个函数,可以调用执行。实例的元表是cls,如果调用实例没有的字段,会去cls里找
cls设置了元方法__index = cls
如果没有super,则只有一个构造函数方法
如果有super,cls的元表是super,元表的元方法也正确的设置了
所以,在Test2是继承自Test的,它的实例test调用doSomething,找不到,去元表里找,元表发现自己有父类,去父类里找,成功找到。

多继承

如果我想要继承多个父类,怎么办?

思路就是将元方法改成函数


local function search(key, tables)
    for _, super in ipairs(tables) do
        if super[key] then
            return super[key]
        end
    end
    return nil
end

local function class(...)
    local cls = { ctor = function () end}
    local supers = {...}
    setmetatable(cls, {__index = function (_, key)
        -- 在查找table的时候,会把table的key传进来
        return search(key, supers)
    end})
    
    function cls.new(...)
        local instance = {}
        setmetatable(instance, {__index = cls})
        instance:ctor(...)
        return instance
    end
    return cls
end

local Human = class()
function Human:life()
    print("almost 100 years.")
end
local Programmer = class()
function Programmer:coding()
    print("sub 1 year.")
end
local My = class(Human, Programmer)
local You = My.new()
You:life()
You:coding()
almost 100 years.
sub 1 year.

解析:

在You里找不到life和coding字段,去找元表cls,调用元方法__index,__index调用函数search,把所有的父类都找一遍
成功找到

posted @ 2019-12-23 11:41  萧蔷ink  阅读(516)  评论(0编辑  收藏  举报