UH2S1L11——元表

元表

任何表变量都可以作为另一个表变量的元表,任何表变量都可以有自己的元表
当我们对有元表的表中进行一些特定操作时,会执行其元表中的内容,即元表内存储的特定方法将改变其对应表的特定方法

在Lua的 table​ 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table​ 进行操作(比如相加)。

因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。

例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。

当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add​ 的字段,
若找到,则调用对应的值。 __add​ 等即时字段,其对应的值(往往是一个函数或是 table)就是"元方法"。

———— Lua 元表(Metatable) | 菜鸟教程 (runoob.com)

本章代码关键字

1
2
3
4
5
6
7
8
9
setmetatable()        --设置元表
getmetatable() --获取元表
__tostring --设置表转字符串的关键字
__call --设置使表能够函数一样执行的方法
-- 运算符重载关键字请直接看相关部分
__index --设置当外部索引某个索引查找值但表内不存在该索引时,会通过表的元表的__index指向的表继续寻找
__newindex --当为表的某个索引赋值时,如果表不存在某个索引,则会将值复制到表的元表的__newindex指向的表中
rawget() --无视元表的__index通过索引查找表中的值
rawset() --无视元表的__newindex为表的某个索引赋值

设置元表

使用setmetatable(table, metatable)​给指定表设置元表。第一个参数是要设置元表的表,第二个参数是要作为元表的表

1
2
3
local meta = {}
local myTable = {}
setmetatable(myTable, meta)

不过用空表设置为一个表的元表是没有作用的

特定操作 __tostring

当把非字符串或者数字的变量传入print​函数时,会自动将其转为字符串再输出,而没有元表的表直接转字符串会是一些列表号

1
2
3
4
local meta = {}
local myTable = {}
setmetatable(myTable, meta)
print(myTable)
1
table: 00C19C40

而我们为表设置元表时,可以在表的元表里实现转字符串的方法,这个方法名就是__tostring​,使用它可以修改转字符串的逻辑

1
2
3
4
5
6
7
8
9
print("**********特定操作-__tostring************")
local meta2 = {
__tostring = function()
return "唐老狮"
end
}
local myTable2 = {}
setmetatable(myTable2, meta2)
print(myTable2)
1
2
**********特定操作-__tostring************
唐老狮

但是如果想让表转字符串的方法能够调用表内的变量的话,需要我们为__tostring​函数添加一个参数,
并默认该参数传入了调用方法的表自己,再调用传入的表中的变量

1
2
3
4
5
6
7
8
9
10
11
print("**********特定操作-__tostring************")
local meta2 = {
__tostring = function(t)
return t.name
end
}
local myTable2 = {
name = "唐老狮2"
}
setmetatable(myTable2, meta2)
print(myTable2)
1
2
**********特定操作-__tostring************
唐老狮2

特定操作 __call

在表对应的元表实现__call​函数,可以让表像函数那样调用(不设置元表不实现该方法强行让表像函数那样调用会报错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print("**********特定操作-__call************")
local meta3 = {
__tostring = function(t)
return t.name
end,
__call = function ()
print("call!")
end
}
local myTable3 = {
name = "唐老狮"
}
setmetatable(myTable3, meta3)
myTable3()
1
2
**********特定操作-__call************
call!

如果我们想为函数添加参数,直接在元表的__call​参数列表里添加一个参数是不可行的,这个参数默认会传入元表对应的表自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print("**********特定操作-__call************")
local meta3 = {
__tostring = function(t)
return t.name
end,
__call = function(a)
print(a)
print("call!")
end
}
local myTable3 = {
name = "唐老狮"
}
setmetatable(myTable3, meta3)
myTable3(1)
1
2
3
**********特定操作-__call************
唐老狮 --很明显这里输出的是myTable3.name
call!

因此,我们需要在元表的__call​参数列表里添加两个参数及以上,第一个参数默认会传入元表对应的表自己,第二个参数起才是外部可用的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print("**********特定操作-__call************")
local meta3 = {
__tostring = function(t)
return t.name
end,
__call = function(self, a)
print(a)
print("call!")
end
}
local myTable3 = {
name = "唐老狮"
}
setmetatable(myTable3, meta3)
myTable3(1)
1
2
3
**********特定操作-__call************
1
call!

特定操作-运算符重载

一般情况下,我们无法让两个表进行相加等运算,但是在其元表里我们可以实现运算符重载,实现相加逻辑,让两个表可以相加

1
2
3
4
5
6
7
8
9
10
print("**********特定操作-运算符重载************")
local meta4 = {
__add = function(t1, t2)
return 5
end
}
local myTable4 = {}
setmetatable(myTable4, meta4)
local myTable5 = {}
print(myTable4 + myTable5)
1
2
**********特定操作-运算符重载************
5
1
2
1        --可见,是meta4的运算符重载起作用了
25 --可见,是meta5的运算符重载起作用了

__add​方法可以需要两个参数,默认传入加号两边的内容

1
2
3
4
5
6
7
8
9
10
print("**********特定操作-运算符重载************")
local meta4 = {
__add = function(t1, t2)
return t1.age + t2.age
end
}
local myTable4 = {age = 1}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2}
print(myTable4 + myTable5)
1
2
**********特定操作-运算符重载************
3

其他运算符的重载与上述例子差不多

算数运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
local meta4 = {
-- +
__add = function(t1, t2)
return t1.age + t2.age
end,
-- -
__sub = function(t1, t2)
return t1.age - t2.age
end,
-- *
__mul = function(t1, t2)
return t1.age * t2.age
end,
-- /
__div = function(t1, t2)
return t1.age / t2.age
end,
-- %
__mod = function(t1, t2)
return t1.age % t2.age
end,
-- ^
__pow = function(t1, t2)
return t1.age ^ t2.age
end
}
local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2, num = 5}

print(myTable4 + myTable5)
print(myTable4 - myTable5)
print(myTable4 * myTable5)
print(myTable4 / myTable5)
print(myTable4 % myTable5)
print(myTable4 ^ myTable5)
1
2
3
4
5
6
3
-1
2
0.5
1
1

当两个表参与算数运算符计算时,只需要有一个表的元表存在运算符重载即可进行计算(只要传入的两个表能够满足运算符重载的逻辑)
运算符左边的表作为第一个参数传入,运算符左边的表作为第二个参数传入(条件运算符不行)

1
2
3
4
5
6
7
8
9
10
11
12
local meta4 = {
__pow = function(t1, t2)
return t1.age ^ t2.age
end
}

local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2, num = 5}

print(myTable4 ^ myTable5)
print(myTable5 ^ myTable4)
1
2
1
2

如果运算时两个表的元表都存在相同算数运算符的重载且逻辑不同,则以运算符左边表的元表的运算符重载为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local meta4 = {
__pow = function(t1, t2)
return t1.age ^ t2.age
end
}
local meta5 = {
__pow = function(t1, t2)
return t1.num ^ t2.num
end
}

local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2, num = 5}
setmetatable(myTable5, meta5)

print(myTable4 ^ myTable5)
print(myTable5 ^ myTable4)

条件运算符

官方没有提供~=​、<​、<=​的重载,需要自己取反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local meta4 = {
-- ==
__eq = function(t1, t2)
return t1.age == t2.age
end,
-- <
__lt = function(t1, t2)
return t1.age < t2.age
end,
-- <=
__le = function(t1, t2)
return t1.age <= t2.age
end,
}

与算数运算符不同,如果要用条件运算符来比较两个表,则两个表的元表必须要一致!才能准确的调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
local meta4 = {
-- ==
__eq = function(t1, t2)
return t1.age == t2.age
end
}
local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 1, num = 5}

print(myTable4 == myTable5)
setmetatable(myTable5, meta4)
print(myTable4 == myTable5)
1
2
false        --添加相同元表前
true --添加相同元表后

从这里我们也可以知道为什么不需要设置>​、>=​的重载,因为这两个就是调换了传入<​、<=​的重载的参数顺序而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local meta4 = {
-- ==
__eq = function(t1, t2)
return t1.age == t2.age
end,
-- <
__lt = function(t1, t2)
return t1.age < t2.age
end,
-- <=
__le = function(t1, t2)
return t1.age <= t2.age
end,
}
local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2, num = 5}

setmetatable(myTable5, meta4)
print(myTable4 > myTable5)
print(myTable4 < myTable5)
print(myTable5 > myTable4)
print(myTable5 < myTable4)
print(myTable4 >= myTable5)
print(myTable4 <= myTable5)
print(myTable5 >= myTable4)
print(myTable5 <= myTable4)
1
2
3
4
5
6
7
8
false
true
true
false
false
true
true
false

拼接运算符

1
2
3
4
5
6
7
8
9
10
local meta4 = {
__concat = function(t1, t2)
return t1.age .. t2.age
end
}
local myTable4 = {age = 1, num = 2}
setmetatable(myTable4, meta4)
local myTable5 = {age = 2, num = 5}

print(myTable4 .. myTable5)
1
12

特定操作 __index​ 和 __newindex

__index

__index​是当元表对应的表找不到某一个索引时,会回到元表里,__index​指定的表去找索引 __index​****最好在外部赋值!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 无__index
local meta6 = {
age = 6
}
local myTable6 = {}
setmetatable(myTable6, meta6)
print(myTable6.age)

-- __index指向自己
local meta6 = {
age = 6
}
meta6.__index = meta6
local myTable6 = {}
setmetatable(myTable6, meta6)
print(myTable6.age)

-- __index指向新表
local meta6 = {}
meta6.__index = {age = 6}
local myTable6 = {}
setmetatable(myTable6, meta6)
print(myTable6.age)
1
2
3
nil
6 --通过元表中__index提供的表找到的age
6 --通过元表中__index提供的表找到的age

注意!不建议将 __index写在元表内部,尤其是将 __index赋值为元表自己时, __index应当在外面进行赋值,否则会出现值错误的情况,
可能的原因是在表内为__index​赋值时,元表自己还没有赋值完毕导致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local meta6 = {
age = 1,
__index = meta6
}
local myTable6 = {}
setmetatable(myTable6, meta6)
print(myTable6.age)

local meta6 = {
age = 6
}
local meta6 = {
age = 1,
__index = meta6
}
local myTable6 = {}
setmetatable(myTable6, meta6)
print(myTable6.age)
1
2
nil        --原本应当是1,但是这里没有赋值
6 --原本应当是1,但是这里却是之前meta6的值

__index是可以赋值元表自己的元表,因此当外部获取表的某个索引的值而表不存在那个索引时,外部可以找到表的元表的元表获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
local meta6Father = {
age = 1
}
meta6Father.__index = meta6Father

local meta6 = {}
meta6.__index = meta6Father
setmetatable(meta6, meta6Father)

local myTable6 = {}
setmetatable(myTable6, meta6)

print(myTable6.age)
1
1

__newindex

__newindex​ 当为表的某个索引赋值时,如果表不存在某个索引,则会将值复制到表的元表的__newindex​指向的表中

1
2
3
4
5
6
local meta7 = {}
meta7.__newindex = {}
local myTable7 = {}
setmetatable(myTable7, meta7)
myTable7.age = 1
print(myTable7.age)
1
1

获取元表

1
2
3
4
5
6
local meta = {
age = 1,
}
local myTable = {}
setmetatable(myTable, meta)
print(getmetatable(myTable).age)
1
1

无视元表__index查找

1
2
3
4
5
6
7
8
9
10
local meta = {
age = 1,
}
meta.__index = meta
local myTable = {}
local myTable1 = { age = 1 }
setmetatable(myTable, meta)
print(myTable.age)
print(rawget(myTable1, "age"))
print(rawget(myTable, "age"))
1
2
3
1
1
nil

无视元表__newIndex赋值

1
2
3
4
5
6
7
8
local meta7 = {}
meta7.__newindex = {}
local myTable7 = {}
setmetatable(myTable7, meta7)
myTable7.age = 1
print(myTable7.age)
rawset(myTable7, "age", 2)
print(myTable7.age)
1
2
nil
2