29、Lua中的元表与元方法

Lua中的元表与元方法

  • 前言
  • 算术类的元方法
  • __tostring元方法
  • 如何保护我们的 "奶酪"——元表
  • __index元方法
  • __newindex元方法

前言

Lua中每个值都可具有元表。 元表是普通的Lua表,定义了原始值在某些特定操作下的行为。你可通过在值的原表中设置特定的字段来改变作用于该值的操作的某些行为特征。

例如,当数字值作为加法的操作数时,Lua检查其元表中的”__add”字段是否有个函数。如果有,Lua调用它执行加法。

我们称元表中的键为事件(event),称值为元方法(metamethod)。前述例子中的事件是”add”,元方法是执行加法的函数。

可通过函数getmetatable查询任何值的元表。

在table中,我可以重新定义的元方法有以下几个:

__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

Lua中的每一个表都有其Metatable。Lua默认创建一个不带metatable的新表

t = {}
print(getmetatable(t)) --> nil

可以使用setmetatable函数设置或者改变一个表的metatable

t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

任何一个表都可以是其他一个表的metatable,一组相关的表可以共享一个metatable(描述他们共同的行为)。一个表也可以是自身的metatable(描述其私有行为)。

接下来就介绍介绍如果去重新定义这些方法。

算术类的元方法

现在我使用完整的实例代码来详细的说明算术类元方法的使用。

Set = {}
local mt = {} -- 集合的元表-- 根据参数列表中的值创建一个新的集合
function Set.new(l)local set = {}setmetatable(set, mt)for _, v in pairs(l) do set[v] = true endreturn set
end-- 并集操作
function Set.union(a, b)local retSet = Set.new{} -- 此处相当于Set.new({})for v in pairs(a) do retSet[v] = true endfor v in pairs(b) do retSet[v] = true endreturn retSet
end-- 交集操作
function Set.intersection(a, b)local retSet = Set.new{}for v in pairs(a) do retSet[v] = b[v] endreturn retSet
end-- 打印集合的操作
function Set.toString(set)local tb = {}for e in pairs(set) dotb[#tb + 1] = eendreturn "{" .. table.concat(tb, ", ") .. "}"
endfunction Set.print(s)print(Set.toString(s))
end

现在,我定义“+”来计算两个集合的并集,那么就需要让所有用于表示集合的table共享一个元表,并且在该元表中定义如何执行一个加法操作。首先创建一个常规的table,准备用作集合的元表,然后修改Set.new函数,在每次创建集合的时候,都为新的集合设置一个元表。代码如下:

Set = {}
local mt = {} -- 集合的元表-- 根据参数列表中的值创建一个新的集合
function Set.new(l)local set = {}setmetatable(set, mt)for _, v in pairs(l) do set[v] = true endreturn set
end

在此之后,所有由Set.new创建的集合都具有一个相同的元表,例如:

local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
print(getmetatable(set1))
print(getmetatable(set2))
assert(getmetatable(set1) == getmetatable(set2))

最后,我们需要把元方法加入元表中,代码如下:

mt.__add = Set.union

这以后,只要我们使用“+”符号求两个集合的并集,它就会自动的调用Set.union函数,并将两个操作数作为参数传入。比如以下代码:

local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
local set3 = set1 + set2
Set.print(set3)

在上面列举的那些可以重定义的元方法都可以使用上面的方法进行重定义。现在就出现了一个新的问题,set1和set2都有元表,那我们要用谁的元表啊?虽然我们这里的示例代码使用的都是一个元表,但是实际coding中,会遇到我这里说的问题,对于这种问题,Lua是按照以下步骤进行解决的:

  1. 对于二元操作符,如果第一个操作数有元表,并且元表中有所需要的字段定义,比如我们这里的__add元方法定义,那么Lua就以这个字段为元方法,而与第二个值无关;
  2. 对于二元操作符,如果第一个操作数有元表,但是元表中没有所需要的字段定义,比如我们这里的__add元方法定义,那么Lua就去查找第二个操作数的元表;
  3. 如果两个操作数都没有元表,或者都没有对应的元方法定义,Lua就引发一个错误;

以上就是Lua处理这个问题的规则,那么我们在实际编程中该如何做呢?
比如set3 = set1 + 8这样的代码,就会打印出以下的错误提示:

lua: test.lua:16: bad argument #1 to 'pairs' (table expected, got number)

但是,我们在实际编码中,可以按照以下方法,弹出我们定义的错误消息,代码如下:

function Set.union(a, b)if getmetatable(a) ~= mt or getmetatable(b) ~= mt thenerror("metatable error.")endlocal retSet = Set.new{} -- 此处相当于Set.new({})for v in pairs(a) do retSet[v] = true endfor v in pairs(b) do retSet[v] = true endreturn retSet
end

当两个操作数的元表不是同一个元表时,就表示二者进行并集操作时就会出现问题,那么我们就可以打印出我们需要的错误消息。

上面总结了算术类的元方法的定义,关系类的元方法和算术类的元方法的定义是类似的,这里不做累述。

__tostring元方法

写过Java或者C#的人都知道,Object类中都有一个tostring的方法,程序员可以重写该方法,以实现自己的需求。在Lua中,也是这样的,当我们直接print(a)(a是一个table)时,是不可以的。那怎么办,这个时候,我们就需要自己重新定义tostring元方法,让print可以格式化打印出table类型的数据

函数print总是调用tostring来进行格式化输出,当格式化任意值时,tostring会检查该值是否有一个tostring的元方法,如果有这个元方法,tostring就用该值作为参数来调用这个元方法,剩下实际的格式化操作就由__tostring元方法引用的函数去完成,该函数最终返回一个格式化完成的字符串。例如以下代码:

如何保护我们的 “奶酪”——元表

我们会发现,使用getmetatable就可以很轻易的得到元表,使用setmetatable就可以很容易的修改元表,那这样做的风险是不是太大了,那么如何保护我们的元表不被篡改呢?在Lua中,函数setmetatable和getmetatable函数会用到元表中的一个字段,用于保护元表,该字段是 metatable。当我们想要保护集合的元表,是用户既不能看也不能修改集合的元表,那么就需要使用metatable字段了;当设置了该字段时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误;如以下演示代码:

function Set.new(l)local set = {}setmetatable(set, mt)for _, v in pairs(l) do set[v] = true endmt.__metatable = "You cannot get the metatable" -- 设置完我的元表以后,不让其他人再设置return set
endlocal tb = Set.new({1, 2})
print(tb)print(getmetatable(tb))
setmetatable(tb, {})

上述代码就会打印以下内容:

{1, 2}
You cannot get the metatable
lua: test.lua:56: cannot change a protected metatable

__index元方法

是否还记得当我们访问一个table中不存在的字段时,会返回什么值?默认情况下,当我们访问一个table中不存在的字段时,得到的结果是nil。但是这种状况很容易被改变;Lua是按照以下的步骤决定是返回nil还是其它值得:

  1. 当访问一个table的字段时,如果table有这个字段,则直接返回对应的值;
  2. 当table没有这个字段,则会促使解释器去查找一个叫__index的元方法,接下来就就会调用对应的元方法,返回元方法返回的值;
  3. 如果没有这个元方法,那么就返回nil结果;

下面通过一个实际的例子来说明__index的使用。假设要创建一些描述窗口,每个table中都必须描述一些窗口参数,例如颜色,位置和大小等,这些参数都是有默认值得,因此,我们在创建窗口对象时可以指定那些不同于默认值得参数。

Windows = {} -- 创建一个命名空间-- 创建默认值表
Windows.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}}Windows.mt = {} -- 创建元表-- 声明构造函数
function Windows.new(o)setmetatable(o, Windows.mt)return o
end-- 定义__index元方法
Windows.mt.__index = function (table, key)return Windows.default[key]
endlocal win = Windows.new({x = 10, y = 10})
print(win.x)               -- >10 访问自身已经拥有的值
print(win.width)          -- >100 访问default表中的值
print(win.color.r)          -- >255 访问default表中的值

根据上面代码的输出,结合上面说的那三步,我们再来看看,print(win.x)时,由于win变量本身就拥有x字段,所以就直接打印了其自身拥有的字段的值;print(win.width),由于win变量本身没有width字段,那么就去查找是否拥有元表,元表中是否有 index对应的元方法,由于存在index元方法,返回了default表中的width字段的值,print(win.color.r)也是同样的道理。

在实际编程中,__index元方法不必一定是一个函数,它还可以是一个table。当它是一个函数时,Lua以table和不存在key作为参数来调用该函数,这就和上面的代码一样;当它是一个table时,Lua就以相同的方式来重新访问这个table,所以上面的代码也可以是这样的:

-- 定义__index元方法
Windows.mt.__index = Windows.default

__newindex元方法

newindex元方法 与index类似,newindex用于更新table中的数据,而index用于查询table中的数据。当对一个table中不存在的索引赋值时,在Lua中是按照以下步骤进行的:

Lua解释器先判断这个table是否有元表;

  1. 如果有了元表,就查找元表中是否有__newindex元方法;如果没有元表,就直接添加这个索引,然后对应的赋值;
  2. 如果有这个__newindex元方法,Lua解释器就执行它,而不是执行赋值;
  3. 如果这个__newindex对应的不是一个函数,而是一个table时,Lua解释器就在这个table中执行赋值,而不是对原来的table;

那么这里就出现了一个问题,看以下代码:

local tb1 = {}
local tb2 = {}tb1.__newindex = tb2
tb2.__newindex = tb1setmetatable(tb1, tb2)
setmetatable(tb2, tb1)tb1.x = 10

发现什么问题了么?是不是循环了,在Lua解释器中,对这个问题,就会弹出错误消息,错误消息如下:

loop in settable

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/819787.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#引用外部组件的常用方法

我们在开发程序过程中,时常会使用到第三方组件,比如一些通信、UI组件等。常用的引用方法有下面几种。 01 NuGet引用 NuGet是.NET的一个包管理平台,很多开源组件会通过NuGet进行管理和发布。比如我们常用的S7NetPlus等。 从NuGet中引用组件…

零基础学Python专栏文章导航站

零基础学Python专栏文章导航站 专栏导读零基础入门篇 专栏导读 本文是零基础学Python的文章导航站。专栏分为零基础入门篇、模块篇、网络爬虫篇、Web开发篇、办公自动化篇、数据分析篇… 为了方便专栏订阅者更方便的阅读专栏文章,点击链接即可跳转到具体文章&#…

FL Studio v21.2.3.4004 中文永久版网盘下载(含Key.reg注册表补丁)

软件介绍 FL Studio21水果编曲软件汉化版是一款专业的音乐制作软件,被广泛地应用于电子音乐、hip-hop、流行乐等多种音乐类型的制作。该软件提供了丰富的音频编曲工具和音乐效果器,让用户可以轻松地创作出高品质的音乐作品。同时,这也是一款…

“我,月薪4500,副业收入2w”:用Python做副业,到底有多赚钱?

现在在年轻人打工的第一目标是什么? 就是:搞钱!搞钱!搞钱! 但赚钱谈何容易,很多人工作只有一点“死”工资,每月再扣除房租水电、花呗信用卡的钱,能用的钱真的不多了,更…

Linux yum搭建Keepalived,2 台机器都有虚拟 IP 问题

文章目录 Keepalived 搭建一、安装二、keepalived配置1、配置文件详解global_defs模块参数vrrp_instance模块参数vrrp_script模块参数 2、修改配置文件3、启动服务 Tips:1️⃣问题:两台机器上面都有VIP的情况2️⃣完整配置文件 Keepalived 搭建 服务IP服务器Keepal…

van-uploader 在app内嵌的webview中的一些坑

问题: 部分版本在ios 中没有问题,但是安卓中不触发图片选择和拍照(之前是可以的,可能是没有锁定版本,重新发版导致的)。在ios中下拉文案是英文,html配置lang等于 zh 也没有用,ios里…

Linux实现cp指令(4.14)

参数&#xff1a;argc是参数总个数 argv是数组的指针&#xff0c;例如argv[0]是cp&#xff0c;argv[1]是src.c&#xff0c;argv[2]是dec.c。 思路&#xff1a; 打开src.c读src.c到buf打开/创建desc.c将buf写入des.cclose两个文件 #include <sys/types.h> #include <…

有依赖的的动态规划问题

题目 题型分析 这是比较典型的动态规划的问题。动态规划是什么呢&#xff1f;本质上动态规划是对递归的优化。例如&#xff0c;兔子数列&#xff1a;f(x) f(x - 1) f(x -2), 我们知道 f 代表了计算公式&#xff0c;这里解放思想一下&#xff0c;如果 f 替换为数组&#xff0…

信息系统项目管理师——成本管理计算专题(一)

常见考点如下: ①问项目预算、BAC、成本基准、应急储备、管理储备的含义及它们之间的区别 ②给出成本基准和管理储备求项目预算&#xff0c;或者给出预算求成本基准等等 ③看图找 PV、AC、EV、SV、CV、BAC、EAC、ETC等 ④根据题干求项目的PV、AC、EV、SV、CV、BAC、EAC、ETC等 …

k8s高可用集群部署介绍 -- 理论

部署官网参考文档 负载均衡参考 官网两种部署模式拓扑图和介绍 介绍两种高可用模式 堆叠 拓扑图如下&#xff08;图片来自k8s官网&#xff09;&#xff1a; 特点&#xff1a;将etcd数据库作为控制平台的一员&#xff0c;由于etcd的共识算法&#xff0c;所以集群最少为3个&…

产业园区综合计费解决方案/预付费系统/用户侧能源计量及收费/无人值守远程抄表收费

安科瑞薛瑶瑶18701709087 园区管理的难点 ◆计费方式多样&#xff1a;园区类型多样&#xff0c;计费逻辑存在多样&#xff0c;工业电价、商业电价、两部制电价等 ◆缴费方式多样&#xff1a;租户性质不同&#xff0c;存在公对公&#xff0c;私对公&#xff0c;线下支付&…

LeetCode 73.矩阵置零————2024 春招冲刺百题计划

给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]] 示例 2&#xff1a; 输入&#xff1a;matrix […

华为OD机试 - 连续天数的最高利润额(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

高效、稳定、兼容:中国星坤MINI PCIE连接器优势明显

电子设备的性能要求日益提高&#xff0c;尤其是在数据传输和连接稳定性方面。中国星坤推出的MINI PCIE连接器&#xff0c;以其出色的性能和显著的优势&#xff0c;迅速成为行业内的佼佼者&#xff0c;为现代电子设备提供了高效、稳定的连接解决方案。 在性能方面&#xff0c;中…

限制登录Linux服务器的几种方式

一.第一种方法 通过修改TCP Wrappers服务访问控制来实现限制登录Linux 1.这里以sshd服务为例&#xff0c;配置完成后&#xff0c;只允许配置允许的IP才能ssh连接本机服务器&#xff0c;其他IP拒绝判断某一个基于tcp协议的服务是否支持tcp_wrapper&#xff0c;要先判断它是否支…

登录页界面设计详细教程:打造令人印象深刻的用户登录体验

我们日常使用的软件都会有注册和登录页&#xff0c;为什么注册登录页面是必不可少的呢&#xff1f;对于企业&#xff0c;目的是将访客转化为产品用户&#xff0c;有助于获得用户画像和各种数据&#xff0c;针对用户的个性化服务进行产品的迭代更新。对于个人&#xff0c;则根据…

鸿蒙应用开发之下拉菜单选择组件

前面学习了搜索框组件,接着下来学习下位菜单选择组件。它实现了一个下拉式的菜单,当用户点击下拉菜单中某一项,就会被选中。 这个组件的界面大体如下: 这个组件可以设置每一项的图标,以及文本显示。由于它是下拉式的菜单,所以候选的内容建议不要太多,否则会滚动比较麻烦…

Redis-键值设计

Redis-键值设计 1.设置key的规范 遵循基本格式&#xff1a;【业务名称】&#xff1a;【数据名】&#xff1a;【id】 可读性强&#xff0c;在客户端的情况下使用:如果前缀相同会分目录层级长度不超过44字节 string数据结构的三种类型&#xff0c;在44字节之内是embstring 内存…

【零基础学数据结构】链表

目录 1.链表的概念 ​编辑 2.链表的雏形 ​编辑 3.链表的组成 ​编辑 4.链表代码 4.1创建节点 4.2链表的打印 4.3链表的尾插 4.4链表的头插 4.5链表的尾删 4.6链表的头删 4.7链表的查找 4.8链表在指定位置之前插⼊数据 4.9链表在指定位置之后插⼊数据 4.9-1删除pos节点 4.9…

做抖音小店保证金可以不交吗?不交保证金,会有什么后果?

哈喽~我是电商月月 说到最赚钱的软件&#xff0c;大家第一个想的就是抖音了&#xff0c;很多不想直播&#xff0c;但又想在抖音上赚钱的人就选择了抖音小店 但普通人创业&#xff0c;开店遇到的第一个困难就是类目保证金的缴纳 几千块钱虽然能拿的出来&#xff0c;但怕就怕在…