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 |