Lua中的元表里的__index和__newindex

在lua中有一个概念叫做"元表"(metatable)。这是一种特殊的表,里面包含一些函数属性,而这个表一旦通过setmetatable绑定了一个普通表,这个普通表的索引就会被施加一些"魔法":当试图根据索引获取该表的某个不存在的元素的值时不再返回nil而是运行某个函数,或返回其它表的元素;当试图给一个表中不存在的索引赋值时,不一定给表添加所要添加的新元素。这两个"魔法",是由元表中的__index和__newindex施加的。

关于元表的基本概念,请参阅元表

一、元表里关于索引的基本函数

将元表mt绑定普通表x的语句是setmetatable(x, mt)。在这里,mt是否会施加x索引方面的"魔法",取决于mt.__indexmt.__newindex的定义。基本规则如下:

(一)、__index

对于获取x中的元素的语句,如print(x[i]),如果x[i]存在,则正常返回x中索引为i的元素;如果不存在,则分三种情况:

1、mt.__index是一个函数

mt.__index的函数应当有两个参数,即mt.__index = function(table, i)...,运行时,table即为mt绑定的普通表x。此时,x[i]的返回值即为mt.__index(x, i)的计算结果。

2、mt.__index是一个表(table)

此时,x[i]的返回值即为mt.__index[i]的计算结果,即mt.__index表中索引为i的元素。这相当于x自己没有答案,于是就"抄"mt.__index的答案。

3、mt.__index没定义,或者不是表也不是函数

此时返回nil。

(二)、__newindex

对于给x中的元素赋值的语句,如x[i] = v,如果x[i]存在,则正常赋值;如果不存在,则分三种情况:

1、mt.__newindex是一个函数

mt.__newindex的函数应当有三个参数,即mt.__newindex = function(table, i, v)...。运行时,table即为mt绑定的普通表x。此时,运行mt.__newindex(x, i, v)。除非函数里有rawset(下一小节会提及),否则不会给表x添加新元素。

2、mt.__newindex是一个表(table)

此时,x[i]=v运行时,会给mt.__newindex[i]赋值。这相当于本该送给x的礼物,被mt.__newindex"偷"走了。

3、mt.__newindex没定义,或者不是表也不是函数

如果不定义(或定义为nil),正常赋值;如果定义为其他类型的对象或变量,如数值,字符串,程序可能出错。不建议定义为表或函数或nil以外的东西。

(三)rawget和rawset

如果无论在哪种情况下,都不希望使用__index,则使用rawget(x, i)返回表x中的i索引元素。如果不存在就返回nil。同样,如果不希望使用__newindex,则用rawset(x, i, v)x中的i索引赋值v。如果不存在,就添加新元素。

二、通过一些简单的例子详解元表索引函数

(一)__index

这里,令普通表x1x2x3拥有三个元素,索引值为1~3。用不同的元表mt1mt2mt3绑定它,观察效果。

代码:

lua 复制代码
print("Three cases of __index:")
print("Case 1, __index badly defined")
x1 = {"a","b","c"}
mt1 = {}
setmetatable(x1, mt1)
mt1.__index = nil
print("x1[1] = ", x1[1]) -- this element exists
print("x1[4]=", x1[4]) -- this element does not exist
print("Case 2, __index is a function")
x2 = {"a","b","c"}
mt2 = {}
setmetatable(x2, mt2)
mt2.__index = function(table, i)
    return i
    end
print("x2[1] = ", x2[1]) -- since x2[1] is already defined, it won't care about __index
print("x2[4]=", x2[4]) -- function is run
print("Case 3, __index is a table")
x3 = {"a","b","c"}
mt3 = {}
setmetatable(x3, mt3)
mt3.__index = {"w","x","y","z"}
print("x3[1] = ", x3[1]) -- since x3[1] is already defined, it won't care about __index
print("x3[4]=", x3[4]) -- table mt3.__index is retrieved.

输出:

复制代码
Three cases of __index:
Case 1, __index badly defined
x1[1] =  a
x1[4]= nil
Case 2, __index is a function
x2[1] =  a
x2[4]= 4
Case 3, __index is a function
x3[1] =  a
x3[4]= z

先看Case 1:

mt1.__index是nil,但x1[1]存在,所以x1[1]返回"a",但x1[4]不存在所以返回nil。

再看Case 2:

mt2.__index是一个函数,返回索引值。由于x2[1]存在,所以即使mt2.__index已定义,运算x2[1]时也不会理会mt2.__index。所以x2[1]返回"a",但x2[4]不存在所以通过mt2.__index定义的函数返回i,即4。

最后看Case 3:

mt3.__index是一个表。x3[1]仍返回"a",x3[4]不存在,所以在表mt3.__index中找索引为4的元素,即"z"。

(二)__newindex

代码:

lua 复制代码
print("Three cases of __newindex:")
print("Case 1, __newindex is badly defined")
x1 = {"a", "b", "c"}
mt1 = {}
setmetatable(x1, mt1)
mt1.__newindex = nil
setmetatable(x1, mt1)
x1[4] = "d"
print("x1[4]=", x1[4])
print("Case 2, __newindex is a function")
x2 = {"a", "b", "c"}
mt2 = {}
setmetatable(x2, mt2)
mt2.__newindex = function(table, i, v)
    -- table[i] = v .. v   -- Don't run this. It leads to C stackflow. 
    -- Because: when running table[i] = v it runs mt2.__newindex function, and in this function, table[i]=v runs 
    -- so trigger another mt2.__newindex which is an infinite recursion
    rawset(table, i, v .. v)
    --there is another function called rawget(table, i)
    end
X2[1] = "new a"
x2[4] = "d"
print("x2[1]=", x2[1])
print("x2[4]=", x2[4])
print("Case 3, __newindex is a table")
x3 = {"a", "b", "c"}
mt3 = {}
setmetatable(x3, mt3)
y3 = {}
mt3.__newindex = y3
x3[4] = "d"
print("x3[4]=", x3[4])
print("y3[4]=", y3[4])

输出:

复制代码
Three cases of __newindex:
Case 1, __newindex is badly defined
x1[4]= d
Case 2, __newindex is a function
x2[1] = new a
x2[4]= dd
Case 3, __newindex is a table
x3[4]= nil
y3[4]= d

先看Case 1,x1[4]不存在,但因为mt1.__newindex是nil,所以正常赋值。x1[4]值为"d"

再看Case 2,mt2.__newindex是函数,给x2[4]用rawset不经过__newindex直接赋值d .. d即"dd"。但由于x2[1]存在,所以仍然正常赋值"new a",没有重复。

最后看Case 3,mt3.__newindex是表y3。所以由于x3[4]不存在,x3[4] = "d"语句运行后,x3[4]仍然不存在,但是y3[4]被赋值"d"了。也就是说,y3[4]把本应赋给x3[4]的值"偷"走了。

(三)一个容易出现的错误

在__newindex的Case 2中,有一段代码我做了屏蔽:table[i] = v .. v -- Don't run this. It leads to C stackflow.,因为如果运行了这段代码,程序会出现堆栈溢出报错。为什么会这样呢?

还是把这段代码截取出来:

lua 复制代码
x2 = {"a", "b", "c"}
mt2 = {}
setmetatable(x2, mt2)
mt2.__newindex = function(table, i, v)
    table[i] = v .. v   
    end
x2[4] = "d"
print("x2[4]=", x2[4])

x2[4] = "d"这一行,由于x2[4]不存在,故在函数mt2.__newindex(table, i, v)中运行table[i] = v .. v时:

1、当i=4时,索引itable,即x2中不存在,所以会运行mt2.__newindex(table, i, v)

2、而此时,table[i]仍然不存在,所以到了table[i]=v .. v时,仍然会运行mt2.__newindex(table, i, v)

3、table[i]始终不被成功赋值,所以永远会在函数mt2.__newindex(table, i, v)里运行mt2.__newindex(table, i, v),造成无限递归,最终堆栈溢出。

所以要切记:在__newindex函数里,给一个表赋值,请不要用常规赋值法,而是务必使用rawset函数!

三、总结

元表可以绑定一个普通表,因此当需要使用该普通表的某个索引的元素时,如果不存在,可以以其它特殊方式返回一个值;同时,需要给普通表的某个不存在的索引赋值时,可以以其它特殊方式赋值或运行特殊函数。但运行此功能时需小心:由于它改变了一些常规语句的运行方式,若使用不当,有时会出现意想不到的错误。所以要使用一些专门的函数避开元表索引"魔法"。

相关推荐
野生技术架构师1 小时前
2026 Java面试宝典(春招/社招/秋招通用):没有前言,只有答案,直接开背
java·开发语言·面试
人道领域2 小时前
【LeetCode刷题日记】131.分割回文串,动态规划优化
java·开发语言·leetcode
z落落2 小时前
C# 接口 interface (多接口实现、类+接口、成员重名)
java·开发语言
知识的宝藏3 小时前
Xpaht self::div 轴语法
开发语言
keykey6.3 小时前
卷积神经网络(CNN):让AI学会“看“
开发语言·人工智能·深度学习·机器学习
IsJunJianXin3 小时前
谷歌搜索cookie NID逆向生成
开发语言·python·google搜索·sgss·nid-cookie·算法生成nid·google-cookie
weikecms4 小时前
美团霸王餐报名API接口
java·开发语言
繁星蓝雨4 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余4 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习