14.元表
14.1 知识点
元表的概念
- 任何表变量都可以作为另一个表变量的元表。
- 任何表变量都可以有自己的元表。
- 可以先简单把元表理解成一张“规则表”。
- 当表进行某些特殊操作时,比如打印、当函数调用、访问不存在的字段、给不存在的字段赋值、做运算符操作,Lua 会去它的元表里找对应的元方法。
元表容易一开始看着绕,其实先抓住一个点就行:
普通表本身没有这些特殊行为,加上元表后,Lua 在某些操作上会多走一层规则。
设置和得到元表
设置元表方法 setmetatable(子表, 元表)
myMetaTable1 = {}
myTable1 = {}
setmetatable(myTable1, myMetaTable1)
setmetatable 有返回值,返回的是第一个参数,也就是 myTable1 这个表本身。
得到元表方法 getmetatable(子表)
print(getmetatable(myTable1)) -- table: 00B91AF0
print(myMetaTable1) -- table: 00B91AF0
元表的__tostring方法 子表当成字符串时用到
- 当子表要被当成字符串使用时,会默认调用子表元表中的
__tostring方法。 - 比如直接打印输出表时,就会走到
tostring相关逻辑。 __tostring函数可以没有参数,也可以有参数。- 有参数时,第一个参数默认就是当前这张子表。
myMetaTable2 = {
-- 无参函数
-- __tostring = function()
-- return "韬老狮1"
-- end
-- 有参函数
__tostring = function(sonTable)
return sonTable.name
end
}
myTable2 = {
name = "韬老狮2"
}
setmetatable(myTable2, myMetaTable2)
print(myTable2) -- 韬老狮2,因为是有参函数
-- 如果元表的 __tostring 使用的是无参函数变量,就是韬老狮1
-- 如果元表的 __tostring 为空,会打印表信息 table: 00CBA480
元表的__call方法 子表当成函数时用到
- 当子表被当成一个函数来调用时,会默认调用子表元表中的
__call方法。 - 如果子表被当成函数调用,但子表的元表里没有实现
__call,就会报错。 __call的第一个参数默认是当前这张子表,后面的参数才是调用时传入的参数。
myMetaTable3 = {
-- __call 方法的第一个参数是元表的子表
__call = function(sonTable, parameter1, parameter2)
print(sonTable) -- 把子表当做字符串使用,如果当前元表没有 __tostring 方法,就直接打印表变量
print(parameter1)
print(parameter2)
print("韬老狮好爱你")
end
}
myTable3 = {
name = "韬老狮3"
}
setmetatable(myTable3, myMetaTable3)
myTable3("一刀999", "两刀666")
-- table: 00BA2830
-- 一刀999
-- 两刀666
-- 韬老狮好爱你
元表的运算重载方法 子表做运算时用到
- 当子表和别的表做运算符操作时,会默认调用子表元表中的一些运算符方法。
- 这个效果有点像 C# 里的运算符重载。
- 运算符方法第一个参数是当前子表,第二个参数是和它一起参与运算的值。
- 做关系比较时,最好让参与比较的两个表使用同一套元表规则,不然很容易和预期不一致,甚至直接报错。
myMetaTable4 = {
-- 运算符 +
__add = function(table1, table2)
return table1.age + table2.age
end,
-- 运算符 -
__sub = function(table1, table2)
return table1.age - table2.age
end,
-- 运算符 *
__mul = function(table1, table2)
return table1.age * table2.age
end,
-- 运算符 /
__div = function(table1, table2)
return table1.age / table2.age
end,
-- 运算符 %
__mod = function(table1, table2)
return table1.age % table2.age
end,
-- 运算符 ^
__pow = function(table1, table2)
return table1.age ^ table2.age
end,
-- 运算符 ==
__eq = function(table1, table2)
return table1.age == table2.age
end,
-- 运算符 <
__lt = function(table1, table2)
return table1.age < table2.age
end,
-- 运算符 <=
__le = function(table1, table2)
return table1.age <= table2.age
end,
-- 运算符 ..
__concat = function(table1, table2)
return table1.age .. table2.age
end
}
myTable41 = { age = 100 }
setmetatable(myTable41, myMetaTable4)
myTable42 = { age = 10 }
print(myTable41 + myTable42) -- 110
print(myTable41 - myTable42) -- 90
print(myTable41 * myTable42) -- 1000
print(myTable41 / myTable42) -- 10
print(myTable41 % myTable42) -- 0
print(myTable41 ^ myTable42) -- 1e+020
setmetatable(myTable42, myMetaTable4) -- 为了使用关系运算符比较,这里也给 myTable42 设置同一个元表
print(myTable41 == myTable42) -- false
print(myTable41 > myTable42) -- true
print(myTable41 <= myTable42) -- false
print(myTable41 .. myTable42) -- 10010
元表的__index表 子表得到某属性时用到
- 当子表想读取某个属性,但子表自己找不到这个属性时,会去子表元表中的
__index里继续找。 __index可以是一张表,也可以是一个函数。- 基础阶段最常见的是把
__index指向一张表,用它来做字段查找兜底。
myMetaTable5 = {
__index = { name = "韬老狮5" }
}
myTable5 = {
}
setmetatable(myTable5, myMetaTable5)
print(myTable5.name) -- 韬老狮5,myTable5 中没有 name,于是去 myMetaTable5.__index 指向的表里找
myMetaTable5.__index = { name = "韬老狮55" } -- 也可以在外面赋值元表的 __index 变量
print(myTable5.name) -- 韬老狮55,myMetaTable5 在外面重新设置了 __index 指向的表
- 注意:有个小坑,如果元表的
__index想指向自己,不建议在表内部直接写。 - 因为表还没创建完成时,变量本身还没有真正指向这张表。
- 所以如果
__index要指向自己,通常在外部赋值更稳。
myMetaTable6 = {
name = "韬老狮6",
__index = myMetaTable6
}
myTable6 = {
}
setmetatable(myTable6, myMetaTable6)
print(myTable6.name) -- nil
myMetaTable6.__index = myMetaTable6 -- 元表的 __index 指向自己,建议在外部赋值
print(myTable6.name) -- 韬老狮6
- 元表中的
__index表如果没有对应属性,还可以继续通过它自己的元表往上找。 - 也就是可以一层一层往上查,直到找到或者最终找不到为止。
myMetaTable7Father = {
name = "韬老狮7",
}
myMetaTable7 = {
}
myTable7 = {
}
setmetatable(myTable7, myMetaTable7)
setmetatable(myMetaTable7, myMetaTable7Father)
myMetaTable7.__index = myMetaTable7 -- 如果 myTable7 找不到属性,去 myMetaTable7 的 __index 表变量找,也就是 myMetaTable7 自己
myMetaTable7Father.__index =
myMetaTable7Father -- 如果 myMetaTable7 还找不到属性,去 myMetaTable7Father 的 __index 表变量找,也就是 myMetaTable7Father 自己
print(myTable7.name) -- 韬老狮7
__index也可以是一个函数。- 如果是函数,它会在子表找不到属性时被调用,并传入两个参数:子表和访问的 key。
myMetaTableIndexFunc = {
__index = function(table, key)
if key == "greeting" then
return "Hello from function!"
else
return "Key not found"
end
end
}
myTableIndexFunc = {}
setmetatable(myTableIndexFunc, myMetaTableIndexFunc)
print(myTableIndexFunc.greeting) -- Hello from function!
print(myTableIndexFunc.otherKey) -- Key not found
元表的__newindex表 子表改某一个属性赋值时用到
- 当子表想给某个不存在的属性赋值时,如果元表中有
__newindex,就会走__newindex的规则。 __newindex可以是一张表,也可以是一个函数。- 如果
__newindex是表,新字段会被写到__newindex指向的表里,而不是当前子表里。
myMetaTable8 = {}
myMetaTable8.__newindex = {}
myTable8 = {}
setmetatable(myTable8, myMetaTable8)
myTable8.name = "韬老狮8" -- 设置了一个 myTable8 本来并不存在的属性 name
print(myTable8.name) -- nil,因为 myTable8 的元表 myMetaTable8 中存在 __newindex 表变量
print(myMetaTable8.__newindex.name) -- 韬老狮8,name 被赋值到元表的 __newindex 表中
__newindex也可以是一个函数。- 如果是函数,它会在子表给不存在的属性赋值时被调用,并传入三个参数:子表、访问的 key、要设置的 value。
myMetaTableNewIndexFunc = {
__newindex = function(table, key, value)
print("Trying to set " .. key .. " to " .. value)
end
}
myTableNewIndexFunc = {}
setmetatable(myTableNewIndexFunc, myMetaTableNewIndexFunc)
myTableNewIndexFunc.name = "人间自有韬哥在" -- Trying to set name to 人间自有韬哥在
rawget和rawset简单了解
rawget(表, key):只从表本身取值,不触发__index。rawset(表, key, value):只往表本身写值,不触发__newindex。- 可以先理解成:它们会绕过元表规则,直接读写当前表本身。
- 这里先只看语法和现象,不展开只读表、安全边界、为什么
__newindex里有时要配合rawset。这些内容放到 Lua 进阶知识的只读表篇再整理。
local meta = {
__index = { name = "默认名字" },
__newindex = {}
}
local t = {}
setmetatable(t, meta)
print(t.name) -- 默认名字,走 __index
print(rawget(t, "name")) -- nil,只查 t 自己
t.age = 18
print(t.age) -- nil,因为 age 写到了 __newindex 指向的表里
print(meta.__newindex.age) -- 18
rawset(t, "age", 20)
print(t.age) -- 20,rawset 直接写 t 自己,不触发 __newindex
print(rawget(t, "age")) -- 20,只查 t 自己
14.2 知识点代码
Lesson14_元表.lua
print("**********元表************")
print("**********知识点一 元表的概念************")
-- 任何表变量都可以作为另一个表变量的元表
-- 任何表变量都可以有自己的元表
-- 可以先简单把元表理解成一张“规则表”
-- 当表进行某些特殊操作时,会去它的元表里找对应的元方法
print("**********知识点二 设置和得到元表************")
-- 设置元表方法 setmetatable(子表, 元表)
myMetaTable1 = {}
myTable1 = {}
setmetatable(myTable1, myMetaTable1)
-- 得到元表方法 getmetatable(子表)
print(getmetatable(myTable1)) -- table: 00B91AF0
print(myMetaTable1) -- table: 00B91AF0
print("**********知识点三 元表的__tostring方法************")
-- 当子表要被当做字符串使用时,会默认调用子表元表中的 __tostring 方法
-- 比如直接打印输出表时,就会走到 tostring 相关逻辑
-- __tostring 函数可以选择有没有参数
-- 没参数的话直接返回字符串即可
-- 有参数的话,第一个参数默认传的是当前子表
myMetaTable2 = {
-- __tostring = function()
-- return "韬老狮1"
-- end
__tostring = function(sonTable)
return sonTable.name
end
}
myTable2 = {
name = "韬老狮2"
}
setmetatable(myTable2, myMetaTable2)
print(myTable2) -- 韬老狮2
-- 如果元表的 __tostring 使用的是无参函数变量,就是韬老狮1
-- 如果元表的 __tostring 为空,会打印表信息 table: 00CBA480
print("**********知识点四 元表的__call方法************")
-- 当子表被当成一个函数来调用时,会默认调用子表元表中的 __call 方法
-- 如果子表被当成函数调用,但子表的元表里没有实现 __call,就会报错
-- __call 方法的第一个参数是当前子表
myMetaTable3 = {
__call = function(sonTable, parameter1, parameter2)
print(sonTable) -- 把子表当做字符串使用,如果当前元表没有 __tostring 方法,就直接打印表变量
print(parameter1)
print(parameter2)
print("韬老狮好爱你")
end
}
myTable3 = {
name = "韬老狮3"
}
setmetatable(myTable3, myMetaTable3)
myTable3("一刀999", "两刀666")
-- table: 00BA2830
-- 一刀999
-- 两刀666
-- 韬老狮好爱你
print("**********知识点五 元表的运算重载方法************")
-- 当子表和别的表做运算符操作时,会默认调用子表元表中的一些运算符方法
-- 这个效果有点像 C# 里的运算符重载
-- 运算符方法第一个参数是当前子表,第二个参数是和它一起参与运算的值
-- 做关系比较时,最好让参与比较的两个表使用同一套元表规则,不然很容易和预期不一致,甚至直接报错
myMetaTable4 = {
-- 运算符 +
__add = function(table1, table2)
return table1.age + table2.age
end,
-- 运算符 -
__sub = function(table1, table2)
return table1.age - table2.age
end,
-- 运算符 *
__mul = function(table1, table2)
return table1.age * table2.age
end,
-- 运算符 /
__div = function(table1, table2)
return table1.age / table2.age
end,
-- 运算符 %
__mod = function(table1, table2)
return table1.age % table2.age
end,
-- 运算符 ^
__pow = function(table1, table2)
return table1.age ^ table2.age
end,
-- 运算符 ==
__eq = function(table1, table2)
return table1.age == table2.age
end,
-- 运算符 <
__lt = function(table1, table2)
return table1.age < table2.age
end,
-- 运算符 <=
__le = function(table1, table2)
return table1.age <= table2.age
end,
-- 运算符 ..
__concat = function(table1, table2)
return table1.age .. table2.age
end
}
myTable41 = { age = 100 }
setmetatable(myTable41, myMetaTable4)
myTable42 = { age = 10 }
print(myTable41 + myTable42) -- 110
print(myTable41 - myTable42) -- 90
print(myTable41 * myTable42) -- 1000
print(myTable41 / myTable42) -- 10
print(myTable41 % myTable42) -- 0
print(myTable41 ^ myTable42) -- 1e+020
setmetatable(myTable42, myMetaTable4) -- 为了使用关系运算符比较,这里也给 myTable42 设置同一个元表
print(myTable41 == myTable42) -- false
print(myTable41 > myTable42) -- true
print(myTable41 <= myTable42) -- false
print(myTable41 .. myTable42) -- 10010
print("**********知识点六 元表的__index表************")
-- 当子表中想得到某一个属性,但找不到对应属性时
-- 会到子表元表中的 __index 里继续找
myMetaTable5 = {
__index = { name = "韬老狮5" }
}
myTable5 = {
}
setmetatable(myTable5, myMetaTable5)
print(myTable5.name) -- 韬老狮5,myTable5 中没有 name,于是去 myMetaTable5.__index 指向的表里找
myMetaTable5.__index = { name = "韬老狮55" } -- 也可以在外面赋值元表的 __index 变量
print(myTable5.name) -- 韬老狮55,myMetaTable5 在外面重新设置了 __index 指向的表
-- 注意:
-- 如果元表的 __index 想指向自己,不建议在表内部直接写
-- 因为表还没创建完成时,变量本身还没有真正指向这张表
-- 所以如果 __index 要指向自己,通常在外部赋值更稳
myMetaTable6 = {
name = "韬老狮6",
__index = myMetaTable6
}
myTable6 = {
}
setmetatable(myTable6, myMetaTable6)
print(myTable6.name) -- nil
myMetaTable6.__index = myMetaTable6 -- 元表的 __index 指向自己,建议在外部赋值
print(myTable6.name) -- 韬老狮6
-- 元表中的 __index 表如果没有对应属性
-- 还可以继续通过它自己的元表往上找
-- 也就是可以一层一层往上查,直到找到或者最终找不到为止
myMetaTable7Father = {
name = "韬老狮7",
}
myMetaTable7 = {
}
myTable7 = {
}
setmetatable(myTable7, myMetaTable7)
setmetatable(myMetaTable7, myMetaTable7Father)
myMetaTable7.__index = myMetaTable7 -- 如果 myTable7 找不到属性,去 myMetaTable7 的 __index 表变量找,也就是 myMetaTable7 自己
myMetaTable7Father.__index =
myMetaTable7Father -- 如果 myMetaTable7 还找不到属性,去 myMetaTable7Father 的 __index 表变量找,也就是 myMetaTable7Father 自己
print(myTable7.name) -- 韬老狮7
-- 元表的 __index 可以是一个函数
-- 如果是函数,它会在子表找不到属性时被调用,并传入两个参数:子表和访问的 key
myMetaTableIndexFunc = {
__index = function(table, key)
if key == "greeting" then
return "Hello from function!"
else
return "Key not found"
end
end
}
myTableIndexFunc = {}
setmetatable(myTableIndexFunc, myMetaTableIndexFunc)
print(myTableIndexFunc.greeting) -- Hello from function!
print(myTableIndexFunc.otherKey) -- Key not found
print("**********知识点七 元表的__newindex表************")
-- 当子表想给某个不存在的属性赋值时,如果元表中有 __newindex,就会走 __newindex 的规则
-- __newindex 可以是一张表,也可以是一个函数
-- 如果 __newindex 是表,新字段会被写到 __newindex 指向的表里,而不是当前子表里
myMetaTable8 = {}
myMetaTable8.__newindex = {}
myTable8 = {}
setmetatable(myTable8, myMetaTable8)
myTable8.name = "韬老狮8" -- 设置了一个 myTable8 本来并不存在的属性 name
print(myTable8.name) -- nil,因为 myTable8 的元表 myMetaTable8 中存在 __newindex 表变量
print(myMetaTable8.__newindex.name) -- 韬老狮8,name 被赋值到元表的 __newindex 表中
-- 元表的 __newindex 也可以是一个函数
-- 如果是函数,它会在子表给不存在的属性赋值时被调用,并传入三个参数:子表、访问的 key、要设置的 value
myMetaTableNewIndexFunc = {
__newindex = function(table, key, value)
print("Trying to set " .. key .. " to " .. value)
end
}
myTableNewIndexFunc = {}
setmetatable(myTableNewIndexFunc, myMetaTableNewIndexFunc)
myTableNewIndexFunc.name = "人间自有韬哥在" -- Trying to set name to 人间自有韬哥在
print("**********知识点八 rawget和rawset简单了解************")
-- rawget(表, key):只从表本身取值,不触发 __index
-- rawset(表, key, value):只往表本身写值,不触发 __newindex
-- 可以先理解成:它们会绕过元表规则,直接读写当前表本身
-- 这里先只看语法和现象
-- 只读表、安全边界、为什么 __newindex 里有时要配合 rawset,后面进阶篇再整理
local meta = {
__index = { name = "默认名字" },
__newindex = {}
}
local t = {}
setmetatable(t, meta)
print(t.name) -- 默认名字,走 __index
print(rawget(t, "name")) -- nil,只查 t 自己
t.age = 18
print(t.age) -- nil,因为 age 写到了 __newindex 指向的表里
print(meta.__newindex.age) -- 18
rawset(t, "age", 20)
print(t.age) -- 20,rawset 直接写 t 自己,不触发 __newindex
print(rawget(t, "age")) -- 20,只查 t 自己
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com