lua–表table、元表metatable、元方法

lua--表table与元表metatable、元方法

  • table
    • 使用table存储数组
    • 使用table存储键值对(hash结构)
    • 上述可见table使用两种方式存储数据
    • table数据结构源码
  • 元表metatable
  • 元方法
    • 通过__index和__newindex实现一个只读table

table

table是lua强大的数据结构,可以运用在大部分场景。
我们先简单的看下table的使用方式。

使用table存储数组

1
2
3
4
5
a = {} --这是一个名为a的空table
b = {1,2,3,4,5} --这是一个可以表示含有五个递增元素的数组的table
print(b[1]) --b[1]的值为1,PS:!!!lua的数组下标是从1开始而不是0
b[6] = 6 --此时table含有的数组由5个长度变成6个长度(是的,与其他语言的vector类似动态分配)
print(#b) --此处打印6,我们可以通过使用#获取b的数组部分长度

使用table存储键值对(hash结构)

1
2
3
4
5
c = {}
c.key1 = 'value1' --我们可以通过 . 添加键值对
c['key2'] = 'value2' --也可以通过[str]添加键值对
c['2'] = 2 --注意这里['2']与数组的[2]是不同的,'2'被作为字符串键处理了
print(c.key1, c['key2'], c['2']) --打印 value1 value2 2

上述可见table使用两种方式存储数据

一般当下标为自然数1,2,3,4时,值会存在数组部分
而下标为-1,0,str等时,会存储在hash部分

要注意的是并不是所有下标为自然数都会存储在数组部分,我们来看一个很有意思的现象:

1
2
3
4
5
6
b = {1,2,3,4,5} --这是一个可以表示含有五个递增元素的数组的table
b[13] = 13
b[14] = 14
for k, v in pairs(b) do --此处含义是将table所有数据打印出来,k为键(数组部分则为下标),v为值
    print(k,v)
end

正常情况下会先打印数组部分,然后再打印hash部分(因为hash是无序 ,所以13不一定比14先打印)
结果如下

1
2
3
4
5
6
7
1   1
2   2
3   3
4   4
5   5
14  14
13  13

14比13先打印,说明了14和13是存放在hash部分的
解释
这是因为lua为了保证数组部分不浪费太多存储空间,保证数组空间利用率>50%,上诉13、14保存在hash部分。
接下来我们做如下操作

1
2
3
for i = 6,12 do --此处含义是将数组的6-12下标填满
    b[i]=i
end

填充完数组后我们再打印table,结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1   1
2   2
3   3
4   4
5   5
6   6
7   7
8   8
9   9
10  10
11  11
12  12
13  13
14  14

13和14的位置居然变‘正常’了,这说明了对table进行增操作的时候,有可能将hash部分中键为自然数的值转存到数组部分。(同理减操作时也可能将数组部分的转存到hash部分)

table数据结构源码

1
2
3
4
5
6
7
8
9
10
11
typedef struct Table {
  CommonHeader;/*GC相关内容,包含下一个GC对象的指针GCObject *next,自身数据类型lu_byte tt,GC标记位 lu_byte mark*/
  lu_byte flags;  /* 1<<p means tagmethod(p) is not present , 元方法标记位,见ltm.h*/
  lu_byte lsizenode;  /* log2 of size of 'node' array, 散列表容量=2^lsizenode */
  unsigned int sizearray;  /* size of 'array' array , 数组容量*/
  TValue *array;  /* array part */
  Node *node;/* 散列桶第一个节点地址*/
  Node *lastfree;  /* any free position is before this position ,指向最后一个未使用的节点*/
  struct Table *metatable;/*元表*/
  GCObject *gclist;/*GC对象链表*/
} Table;

我们跳过一些CG相关的成员变量,只看下面几个

1
2
3
4
5
6
7
  lu_byte flags;  /* 1<<p means tagmethod(p) is not present , 元方法标记位,见ltm.h*/
  lu_byte lsizenode;  /* log2 of size of 'node' array, 散列表容量=2^lsizenode */
  unsigned int sizearray;  /* size of 'array' array , 数组容量*/
  TValue *array;  /* array part */
  Node *node;/* 散列桶第一个节点地址*/
  Node *lastfree;  /* any free position is before this position ,指向最后一个未使用的节点*/
  struct Table *metatable;/*元表*/

带着之前的实验和注释大家对table的数据结构应该大致有个理解了。
由于table的增删在底层做了比较复杂的处理,本菜鸟又没有去仔细看,故感兴趣的朋友可以翻源码了解table是如果让数组和hash部分的数据互相转存的,以及他们的方法。

元表metatable

我们从上节table数据结构可以看到有一个成员变量,它就是元表

1
struct Table *metatable;/*元表*/

可见,元表也是table
我们可以通过一下方式为一个table设置matatable

1
setmetatable(table,matatable)

或则取出一个table的matatable

1
getmetatable(table)

实际上,单纯的表作为元表并没有太大的意义
我们需要的是一个实现了元方法的表作为元表

元方法

元方法实际上是table的一些保留键,一般格式为__xx,我们可以为这些键赋值为函数(部分元方法可以设为table)
例子
通过__index键实现索引元表

1
2
3
4
5
a = {k = 'va'}
b = {kb = 1,k = 'vb'}
mataTb = { __index=b }
setmetatable(a,mataTb)
print(a.kb,a.k)--输出为 1  va

我们为mataTb 的__index键赋值为b
如果我们访问a的一个键时,如果该值不存在,则会判断a有无元表,其元表__index有无值,如果__index的值为一个table b,则会尝试去访问b的键。

__index还可以设为一个函数,则当访问a的键不存在时,会将父table和键以参数方式传给函数fun并将fun的返回值作为访问结果。如:

1
2
3
4
5
6
a = {k = 'va'}
mataTb = { __index = function(tableA, key)--将a和键传给该函数
    return 'key:'..key --此处..是拼接两个字符串
end }
setmetatable(a,mataTb)
print(a.kk,a.k)--输出为 key:kk  va

下面列出所有元方法:

__newindex
__add
__sub
__mul
__div
__mod
__pow
__unm
__idiv
__band
__bor
__bxor
__bnot
__shl
__shr
__concat
__len
__eq
__lt
__le
__call

如何使用可以访问

lua参考手册http://cloudwu.github.io/lua53doc/manual.html#2.4

通过__index和__newindex实现一个只读table

通过元方法,我们可以实现一些数据结构,比如只读table、对象与继承等。
只读表的实现:

1
2
3
4
5
6
7
8
9
10
11
b = {k = 'v1'}
function func(tb, key, val)
    print('你不能修改表')
end
a = {}
setmetatable(a, { __index = b , __newindex = func})
a.k1 = 1
print(a.k, a.k1)
--上述输出为
你不能修改表
v1  nil