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.
[email protected] table table
[email protected] index any
[email protected] 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,把所有的父类都找一遍
成功找到

原文地址:https://www.cnblogs.com/xiaoqiangink/p/12082908.html

时间: 2024-08-04 22:44:09

Lua 学习之基础篇八<Lua 元表(Metatabble)&&继承>的相关文章

Lua 学习之基础篇七&lt;Lua Module,Package介绍&gt;

Lua 之Module介绍 包管理库提供了从 Lua 中加载模块的基础库. 只有一个导出函数直接放在全局环境中: [require]. 所有其它的部分都导出在表 package 中. require (modname) 加载一个模块. 这个函数首先查找 [package.loaded] 表, 检测 modname 是否被加载过. 如果被加载过,require 返回 package.loaded[modname] 中保存的值. 否则,它会为模块寻找加载器. require 遵循 [package.

Lua 学习之基础篇十&lt;Lua 常见的语法规则&gt;

下面讲一些lua 常见的用法和规则,可以为学习理解lua带来帮助. 1. if判断 lua把 nil 和false 视为"假",其他都为"真" 2. 逻辑运算符 and or lua的and or 可以用来构成三元表达式,如下: > = 1 == 1 and 1 or 2 1 但如果是这样写,就不是你想要的结果了: > = 1 == 1 and false or true true 这是因为,and 运算符判定 false不成立,就继续执行了 or 运算

Lua 学习之基础篇五&lt;Lua 之 OS 库&gt;

lua os库提供了简单的跟操作系统有关的功能 1.os.clock() 返回程序所运行使用的时间 local nowTime = os.clock() print("now time is ",nowTime) local s = 0 for i = 1,100000000 do s =s+i end spendTime = os.clock() - nowTime print(string.format("Spend time is : %.2f\n", spe

Lua 学习之基础篇六&lt;Lua 之IO 库&gt;

引言 I/O 库提供了两套不同风格的文件处理接口. 第一种风格使用隐式的文件句柄: 它提供设置默认输入文件及默认输出文件的操作, 所有的输入输出操作都针对这些默认文件. 第二种风格使用显式的文件句柄. 当使用隐式文件句柄时, 所有的操作都由表 io 提供. 若使用显式文件句柄, io.open 会返回一个文件句柄,且所有的操作都由该文件句柄的方法来提供. 表 io 中也提供了三个 和 C 中含义相同的预定义文件句柄: io.stdin io.stdout io.stderr. I/O 库永远不会

Lua 学习之基础篇九&lt;Lua 协同程序(Coroutine)&gt;

引言 讲到协程,首先来介绍一下线程和协程的区别 lua协程和多线程 相同之处:拥有自己独立的桟.局部变量和PC计数器,同时又与其他协程共享全局变量和其他大部分东西 不同之处:一个多线程程序可以同时运行几个线程(并发执行.抢占),而协程却需要彼此协作地运行,并非真正的多线程,即一个多协程程序在同一时间只能运行一个协程,并且正在执行的协程只会在其显式地要求挂起(suspend)时,它的执行才会暂停(无抢占.无并发). 注意: Lua中的协程无法在外部将其停止,有可能导致程序阻塞 运行的是主线程时调用

Python基础篇(八)

key words:私有变量,类静态变量,生成器,导入Python模块,r查看模块可以使用的函数,查看帮助信息,启动外部程序,集合,堆,时间模块,random模块,shelve模块,文件读取等 >>> class Rectangle: ...     def __init__(self): ...         self.__width = 0 ...         self.__height = 0 ...     def setSize(self,width,height): .

一步步学习javascript基础篇(3):Object、Function等引用类型

我们在<一步步学习javascript基础篇(1):基本概念>中简单的介绍了五种基本数据类型Undefined.Null.Boolean.Number和String.今天我们主要介绍下复杂数据类型(即引用数据类型) Object类型 我们用的最多的引用类型就属object类型了,一般用来存储和传输数据是再好不过的.然,它的两种创建方式我们是否了解呢? 1.通过构造函数来创建 如: var obj = new Object(); 在js中的引用类型有个非常灵活的用法,可以动态的附加属性和赋值.

渗透学习笔记--基础篇--sql注入(字符型)

环境:dvwa1.7数据库:mysql前置知识:sql语句(Click me)      在进行sql注入前,我们先熟悉熟悉select语句.一.打开我们的sql终端 二.进入之后可以看到有mysql>我们输入sql语句,即可返回我们想要的结果,注意分号哟!我们使用的dvwa,在我们前几章设置的时候,会在数据库中生成一个dvwa的database:这里我们使用它来进行我们的select 语句:(1)使用dvwa数据库use dvwa;(2)在users表里查询用户名为'admin'的所有信息se

iOS开发学习笔记:基础篇

iOS开发需要一台Mac电脑.Xcode以及iOS SDK.因为苹果设备都具有自己封闭的环境,所以iOS程序的开发必须在Mac设备上完成(当然,黑苹果应该也是可以的,但就需要花很多的精力去折腾基础环境),Xcode是一个集成开发环境,包括了编辑器.调试.模拟器等等一系列方便开发和部署的工具,iOS SDK则是开发应用所必需,不同的SDK分别对应不同的iOS版本或设备,通常我们需要下载多个iOS SDK以确保我们开发的程序能够在不同版本的iOS上正常运行. 创建新工程 Xcode提供了很多种工程模