zoukankan      html  css  js  c++  java
  • 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,把所有的父类都找一遍
    成功找到

  • 相关阅读:
    [Swift]LeetCode101. 对称二叉树 | Symmetric Tree
    [Swift]LeetCode88. 合并两个有序数组 | Merge Sorted Array
    [Swift]LeetCode70. 爬楼梯 | Climbing Stairs
    中国象棋程序的设计与实现(十一)--第2次回答CSDN读者的一些问题
    极速响应Excel数据报表请求的一种方法
    极速响应Excel数据报表请求的一种方法
    中国象棋程序的设计与实现(十)--棋盘的定义和绘制
    中国象棋程序的设计与实现(十)--棋盘的定义和绘制
    中国象棋程序的设计与实现(九)–棋子点,棋子的小窝
    中国象棋程序的设计与实现(九)–棋子点,棋子的小窝
  • 原文地址:https://www.cnblogs.com/xiaoqiangink/p/12082908.html
Copyright © 2011-2022 走看看