元表元方法
(key--value常见翻译为“键值对”,我翻译为索引、值)
每一个tabel都可以附加元表, 元表是带有索引集合的表,它可以改变设置此元表的表的行为。
可以通过元表来修改一个值的行为,使其在面对一个非预定于的操作时执行一个指定的操作。
元方法类似于C语言的运算符重载,主要是为了实现表与表的运算
元表是普通的Lua表,定义了原始值在某些特定操作下的行为。你可通过在值的原表中设置特定的字段来改变作用于该值的操作的某些行为特征。例如,当数字值作为加法的操作数时,Lua检查其元表中的"__add"字段是否有个函数。如果有,Lua调用它执行加法。
我们称元表中的键为事件(event),称值为元方法(metamethod)。前述例子中的事件是"add",元方法是执行加法的函数
元表本质上来说是一种用来存放元方法的table。我们可以通过对应的key来得到value值,作用就是修改一个值的行为(更确切的说,这是元方法的能力),需要注意的是,这种修改会覆盖掉原本该值可能存在的相应的预定义行为。
1. lua中的每个值都可以有一个元表,只是table和userdata可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。
lua代码中只能设置table的元表,至于其他类型值的元表只能通过C代码设置。
多个table可以共享一个通用的元表,但是每个table只能拥有一个元表。
我们称元表中的键为事件(event),称值为元方法(metamethod)。前述例子中的事件是"add",元方法是执行加法的函数。
可通过函数getmetatable查询任何值的元表。
可通过函数setmetatable替换表的元表
lua查找表中的元素时规则如下:
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续
3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值
元表就是,如果一个tableB 调用setmetatable方法设置另外一个tableA作为它的元表,那么当你操作tableB进行如设置键值,查找键值,table间使用运算符号、或者直接传个参数调用等操作时就会变得不一样或者原本不可行操作的现在可行了。而tableA之所以能作为元表,是因为里面包含了特殊的键代表着特殊的含义,代码在运行时会做特殊的处理
Lua中每个值都有一个元表metatable
,这个元表metatable
是一个原始的Lua表table
,元表metatable
用来定义原始值在特定操作下的行为。通过在元表metatable
中的特定域设一些值来改变拥有这个元表metatable
的值的指定操作的行为。我们称元表metatable
中的键名为事件event
,将其中的值叫做元方法metamethod
。
Lua5.1中元表metatable
的表现行为类似于C++语言中的操作符重载,例如:重载__add
元方法metamethod
,来计算两个Lua数组的并集。重载__index
元方法来定义自己的Hash
函数。
通常,Lua中每个值都有一套预定义的操作集合。例如,数字相加、连接字符串、表插入key-value
对等,但无法将两个表相加,无法对函数比较等。此时,可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。
- Lua中每个值都有一个元表
metatable
table
和userdata
可以有各自独立的元表metatable
,其他类型的值共享其类型所属的单一元表。 - Lua中只能设置表
table
的元表metatable
,设置其他类型值得元表必须通过C代码实现。 - Lua在创建新的表
table
时不会创建元表metatable
表与元表
- 表
table
可作为任何值得元表metatable
,一组相关的表可以共享一个通用的元表,通用的元表描述了它们共同的行为。 - 表
table
可作为自己的元表metatable
,用于描述其特有的行为。
表的元表获取与修改
- 使用
setmettable
来设置或修改表table
的元表metatable
- 使用
getmettable
来获取元表
元表的作用:当使用到新表没有的属性或者函数,可以在元表中查找,类似继承
元表的设置:在Lua中新建table的时候不会创建元表。元表需要使用setmetatable(t)才能设置,在Lua中只能设置table类型的元表
元方法:预定义一些元方法,可以在新表数据进行算数运算和关系运算的时候能查找到对应处理的函数
一些预定义元方法的指定:
mt.__add = +
mt.__mul = -
mt.__tostring = print
mt.__eq = =
mt.__lt = <
mt.__le = <=
__index元方法:当访问一个新表不存在的字段,如果元表有指定该元方法的时候,会调用该元方法
作用:构造新表的时候只需要保存一份共享的原型数据,然后通过__index去获取新表没有但原型数据有的字段
(也可以在构造新表的时候填充字段,但是这样会产生更大的开销)
类型:__index可以是一个函数也可以是一个table
绕过:可以使用rawget(t,i)的函数绕过改元方法去做一个不考虑元表的简单访问
__newindex元方法:
作用:用于更新新表不存在的属性的值
类型:当是函数:赋值的时候会调用这个元方法
当是table:复制在这个table中
绕过:rawset(t,k,v)
在于module函数里的模块都是点号实现,需要传入self。
函数访问的是靠近它的变量
当存在同名全局和局部变量的情况下,谁越靠近函数,函数就访问谁,如下:如果调换局部和全局变量的位置,将输出不同的结果
name=33
local name=5
f=function()
name=name+1
print(name) --调换全局,和局部变量name输出不一样的结果
end
f()
do...end
do end方法块直接执行,不必调用,其实do ... end就是执行了一个语句块,并没有什么特殊的含义,它基本上等同于C/C++中的{},需要注意的是在这个{}之间的局部变量,在这个区域之后的位置是没有办法引用的,在lua中也是一样的,只不过在lua中可以随意的定义全局变量,所以在do ... end之间的定义的全局变量,在语句块之后也可以引用。
如下:
do
local localName="这是语句块局部变量"
globalName="这是全局变量"
local create
create = function(n1,n2)
print(n1+n2)
end
create(3,8) --输出11
end
print(localName) --输出nil
print(globalName) --输出这是全局变量
:冒号
冒号的作用就是:定义函数时,给函数的添加隐藏的第一个参数self;调用函数时,默认把当前调用者作为第一个参数传递进去。
使用了冒号之后,就相当于我们刚刚使用点号时一样,只是我们不再需要显式地定义self参数以及主动地传递who参数。
require
多次require同一个脚本返回的是同一个table。(这个原因是require会把结果存放在package.loaded这个table中,所以require执行的时候会优先判断package.loaded里是否存在,如果存在则直接返回,否则才会进行本地加载)
元表:
我的理解中,元表像是一个备用查找表,说白了假设表A的元表是B,那么如果在A中找不到的东西就会尝试在B中去找。
__index:
__index元方法按照之前的说法,如果A的元表是B,那么如果访问了一个A中不存在的成员,就会访问查找B中有没有这个成员。这个过程大体是这样,但却不完全是这样,实际上,即使将A的元表设置为B,而且B中也确实有这个成员,返回结果仍然会是nil,原因就是B的__index元方法没有赋值。按照我的理解,__index方法是用来确定一个表在被作为元表时的查找方法。这么说有点绕。所以:
这样一来,结合上例,来解释__index元方法的含义:
在上述例子中,访问son.house时,son中没有house这个成员,但Lua接着发现son有元表father,于是此时father被当做元表来查找,此时,Lua并不是直接在father中找名为house的成员,而是调用father的__index方法,如果__index方法为nil,则返回nil,如果是一个表(上例中father的__index方法等于自己,就是这种情况),那么就到__index方法所指的这个表中查找名为house的成员,于是,最终找到了house成员。
注:__index方法除了可以是一个表,还可以是一个函数,如果是一个函数,__index方法被调用时将返回该函数的返回值。
到这里,总结一下Lua查找一个表元素时的规则,其实就是如下3个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续
3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值
模块
名为“Test” 的模块代码:
local M = {};
local modelName = ...;
_G[modelName] = M;
function M.test1(num)
print(type(num))
end
function M.test2(self,num)
print(type(num))
end
M.memberVar = 100
return M;
其中 “…”就是传递给模块的模块名,在这里其实就是“Test”这个字符串。
改进后的模块
local M = {};
local modelName = ...;
_G[modelName] = M;
-- 方法1:使用继承
setmetatable(M, {__index = _G});
setfenv(1, M);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;
模块简化
简化前:
local M = {};
local modelName = ...;
_G[modelName] = M;
function M.play()
print("那么,开始吧");
end
function M.quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;
留意一下,这里有一个 local modelName = …
“…”就是传递给模块的模块名,在这里其实就是“game”这个字符串。
还记得之前介绍的全局环境_G吗?我们以”game”作为字段名,添加到_G这个table里。
于是,当我们直接调用game的时候,其实就是在调用_G[“game”]的内容了,而这个内容就是这里的M。
简化后:
local M = {};
local modelName = ...;
_G[modelName] = M;
setfenv(1, M);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;
我们把game.lua这个模块里的全局环境设置为M,于是,我们直接定义函数的时候,不需要再带M前缀。
因为此时的全局环境就是M,不带前缀去定义变量,就是全局变量,这时的全局变量是保存在M里。
所以,实际上,play和quit函数仍然是在M这个table里。
于是,我们连前缀都不用写了,这真是懒到了一个极致,简直就是艺术~
另外,由于当前的全局环境是M,所以, 在这里不需要担心重新定义了已存在的函数名,因为外部的全局变量与这里无关了。
进一步简化:
local M = {};
local modelName = ...;
_G[modelName] = M;
-- 方法1:使用继承
setmetatable(M, {__index = _G});
setfenv(1, M);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;
最简化后:
module(..., package.seeall);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
module函数
module函数 :module函数的作用是创造出一个新的“环境”,在这个模块的所有全局函数都只属于这个环境,如果外部需要调用,我们只能需要使用论点2的方式。而如果,在模块中使用了local 定义变量,很抱歉,这个变量将不能被外部调用
在使用了module函数的脚本,使用require并不能返回一个table,而是一个bool值,这个值告诉你是否加载成功
在module函数执行前,是可以使用上一个“环境”的内容的。例如:在module函数前,有这么一句代码:local print = print; --相当于 local print = _G.print
我们在写一个模块时,需要写以下代码:
local M = {};
local modelName = ...;
_G[modelName] = M;
setmetatable(M, {__index = _G});
setfenv(1, M);
就这几句代码,其实我们可以忽略不写,因为,我们有module函数,它的功能就相当于写了这些代码。
我们修改一下game.lua的内容,如下代码:
module(..., package.seeall);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
注意,前面的几行代码都没了,只留下了一个module函数的调用。
module函数的调用已经相当于之前的那些代码了。
而package.seeall参数的作用就是让原来的_G依然生效,相当于调用了:setmetatable(M, {__index = _G});
再次留意一下,代码末尾的return M也不见了,因为module函数的存在,已经不需要我们主动去返回这个模块的table了。
咱们把module("UI_JCTest2",package.seeall);的package.seeall去掉
--JCTest2.lua文件
module("JCTest2");
function Func1()
print("Func1")
end
local function Func2()
print("Func2")
end
--调用main.lua中
require("JCTest2")
JCTest2:Func1()
结果报错了,编译器会告诉你print这个函数是null,它不存在
那么问题来了,print是一个全局函数(存放在_G)中,那么为什么在这里会为空呢
原因就是代码从下往下执行,当执行到module(“JCTest2”)的时候进入了一个独立的环境,注意,是独立!它不与其他环境有任何环境有任务关系,先前的环境中的全局函数和它也没有任何关系(好像一个自成的小宇宙一样)