由浅入深的理解Lua的数据结构——table

1.Table的几个小知识点

  • table使用关联数组,我们可以使用任意类型的值来作为数组的索引(不能使用nil)
  • table没有固定的大小,可以动态的添加元素

2.table的构造

//初始化表
//1
mytable = {}
//2
_mytable = {a=100,b="123"}

//使用.号赋值
_mytable.a = 110
//使用索引赋值
_mytable["c"]=139

思考一下:如果现在定义了一个table a,将table a赋值给table b,此时它们的内存情况是什么样呢?
ab都会指向同一个内存块,如果a设置为nil,b依旧能访问该内存块的元素,直到b设置为nil后,Lua的垃圾回收机制会清理相应的内存。所以当b在更改table内的值后,a再去访问的时候,值也是改变的。

ab内存

table的for循环写法

for k,v in pairs(mytable) do
  print(i,v)
end

3. 从table结构体分析table属性

typedef struct Table {
  CommonHeader;
  lu_byte flags;  /* 1<

先解释一下上面的各个变量

  1. CommonHeader 为所有可回收资源提供标记头
  2. lu_byte其实是 typedef unsigned char lu_byte,lu_byte flags用于表示表中提供了哪些元方法(元方法是什么?我们下面和元表一起解释)
  3. lu_byte lsizenode 它的值就是表的长度,但是散列表大小的扩增一定是2的幂,如果散列桶数组要扩展的话,也是每次在原大小的基础上乘以2的形式扩展。
  4. struct Table *metatable 也就是元表。

3.1 元表
3.1.1 为什么要用元表

在table中,我们无法对两个table之间进行操作,比如:table a+ table b。为了解决这个问题,就引入了元表的概念,它允许我们改变table的行为

3.1.2 元表的操作流程

当我们在进行表a+b的时候:

  1. 检查两者中是否至少有一者是有元素
    2.查找是否有"_add"字段,如果有,就取得它的值,此时这个值就是我们的元方法,里面编写了add函数

其实逼逼了这么多,我觉得元表和元方法其实就相当于重载,比如_add,我们就是重载了+操作符
也可以将它理解成事件驱动,元表中的键对应着不同的事件名,关联的值是元方法,元方法里就是我们事件对应的操作。


继续接上面的分析

  1. TValue *array是指向数组的指针
    TValue是一个结构体,里面保存着TValuefields,它是一个宏定义,包含value和int值,而value是一个联合体,它可以是指针、数字等等。
typedef struct lua_TValue {
  TValuefields;
} TValue;

TValuefields:
#define TValuefields    Value value; int tt

Value:
typedef union {
  GCObject *gc;
  void *p;
  lua_Number n;
  int b;
} Value;
  1. Node *node 是指向该表的散列桶数组起始位置的指针

3.2 Node类型

每个Node都是一个键值对,里面包含了key和value。tvk是key的值,但是当我们出现hash冲突,此时lua的hash算法比较特殊,一般情况下,我们的hash算法都是根据key算出hash,然后如果有冲突的话,就放在改位置的链表上。而lua不同,当它出现hash冲突的时候,会在hash表中找到一个空的位置x,来存放key,并且让冲突处的节点的nk.next指向x
这意味着什么呢?发生冲突我们无需重新分配空间来存储冲突的key,而是利用hash表上未用过的空白处来存储。刚才我们将key放在了空位置x,如果此时存在另一个key2,它算出的hash值是空位置x,而我们刚才的key只是因为hash冲突了,占用了其他key的位置,这个时候我们就讲key2放在x上,将key再放到另一个空白处

忍不住想总结一波table的实现,和上面我们的Node类型进行对照

3.3 table的实现

lua的table其实由数组段hash两部分组成,当你的key值不会过于离散的时候,lua就会将它存储在数组段(也就是下图的array),反正会存储在hash段(也就是下图的node),这个分割线是以数组段的利用率不低于50%为准。hash段采用闭散列的算法,它将有冲突的key存储在空闲槽中,而不额外分配内存。

table

在我们table结构体中,array和sizearray都是表示数组段。
而lsizenode和node,lastfree是表示hash段。node指向hash表的起始位置,lsizenode是log2(node指向的哈希表的节点数目),lastfree指向node里面最后一个未使用的节点(因为我们在hash冲突的时候,是从后往前查找未使用的节点,lastfree存储最后一个未使用节点就可以方便查找)


如果此时hash表中已经没有空格了,那么lua就会resize这个hash表(等会再谈lua的动态扩增)

typedef union TKey {
  struct {
    TValuefields;
    struct Node *next;  /* for chaining */
  } nk;
  TValue tvk;
} TKey;

typedef struct Node {
  TValue i_val;
  TKey i_key;
} Node;

4. table的创建、查找、动态分配

4.1 table的创建
Table *luaH_new (lua_State *L, int narray, int nhash) {
  Table *t = luaM_new(L, Table);
  luaC_link(L, obj2gco(t), LUA_TTABLE);
  t->metatable = NULL;
  t->flags = cast_byte(~0);
  /* temporary values (kept only if some malloc fails) */
  t->array = NULL;
  t->sizearray = 0;
  t->lsizenode = 0;
  t->node = cast(Node *, dummynode);
  setarrayvector(L, t, narray);
  setnodevector(L, t, nhash);
  return t;
}

lua创建新表的时候先为新表分配内存Table * t = luaM_new(L, Table),然后将表连接到gc上并设置标志位luaC_link(L, obj2gco(t), LUA_TTABLE),然后初始化一些必要的属性,使用setarrayvector为数组段分配内存,setnodevector为hash部分分配内存,最后返回表指针。

4.2 table的查找
const TValue *luaH_get (Table *t, const TValue *key) {
  switch (ttype(key)) {
    case LUA_TNIL: return luaO_nilobject;
    case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
    case LUA_TNUMBER: {
      int k;
      lua_Number n = nvalue(key);
      lua_number2int(k, n);
      if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
        return luaH_getnum(t, k);  /* use specialized version */
      /* else go through */
    }
    default: {
      Node *n = mainposition(t, key);
      do {  /* check whether `key' is somewhere in the chain */
        if (luaO_rawequalObj(key2tval(n), key))
          return gval(n);  /* that's it */
        else n = gnext(n);
      } while (n);
      return luaO_nilobject;
    }
  }
}

table的查找会根据key进行判断,如果key为空就直接返回空,key为字符串就调用luaH_getstr(t, rawtsvalue(key)),key为数字则根据它是否整数调用luaH_getnum(t, k),否则,计算出key的位置,遍历table的node节点,找到对应键所在的节点返回。

因为key为数字比较特殊,所以研究一把luaH_getnum函数的实现

const TValue *luaH_getnum (Table *t, int key) {
  /* (1 <= key && key <= t->sizearray) */
  if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
    return &t->array[key-1];
  else {
    lua_Number nk = cast_num(key);
    Node *n = hashnum(t, nk);
    do {  /* check whether `key' is somewhere in the chain */
      if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk))
        return gval(n);  /* that's it */
      else n = gnext(n);
    } while (n);
    return luaO_nilobject;
  }
}

如果key大于等于1,小于数组的长度,则从数组中取出对应的键值,否则利用hashnum找到key对应的node位置,遍历node链表,返回对应的值

4.3 table的赋值
static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
  Node *mp = mainposition(t, key);
  if (!ttisnil(gval(mp)) || mp == dummynode) {
    Node *othern;
    Node *n = getfreepos(t);  /* get a free place */
    if (n == NULL) {  /* cannot find a free place? */
      rehash(L, t, key);  /* grow table */
      return luaH_set(L, t, key);  /* re-insert key into grown table */
    }
    lua_assert(n != dummynode);
    othern = mainposition(t, key2tval(mp));
    if (othern != mp) {  /* is colliding node out of its main position? */
      /* yes; move colliding node into free position */
      while (gnext(othern) != mp) othern = gnext(othern);  /* find previous */
      gnext(othern) = n;  /* redo the chain with `n' in place of `mp' */
      *n = *mp;  /* copy colliding node into free pos. (mp->next also goes) */
      gnext(mp) = NULL;  /* now `mp' is free */
      setnilvalue(gval(mp));
    }
    else {  /* colliding node is in its own main position */
      /* new node will go into free position */
      gnext(n) = gnext(mp);  /* chain new position */
      gnext(mp) = n;
      mp = n;
    }
  }
  gkey(mp)->value = key->value; gkey(mp)->tt = key->tt;
  luaC_barriert(L, t, key);
  lua_assert(ttisnil(gval(mp)));
  return gval(mp);
}

dummynode是带头结点的指针。
往table中插入新值,先检测key的主位置(main position)是否为空,主位置就是key的哈希值在node中的位置。
如果主位置为空,就直接插入,主位置不为空,检查占领该位置的key的主位置是不是在这个地方,如果不在,则将该key移动到其他空闲位置,将要插入的key插入到这个位置中。如果在这个地方,则将要插入的key插入到一个空槽中。
如果找不到空闲位置放新键值,就rehash函数,扩增hash表的大小,再找出新位置,再调用luaH_set把要插入的key插入到新的哈希表中,直接返回LuaH_set的结果。

4.4 table的动态增长
4.4.1 rehash
//加入key,重新分配hash与array的空间
static void rehash (lua_State *L, Table *t, const TValue *ek) {
  int nasize, na;//nasize前期累计整数key个数,后期做为数组空间大小,na表示数组不为nil的个数
  int nums[MAXBITS+1];  /* 累计各个区间整数key不为nil的个数,包括hash, 例如nums[i]表示累计在[2^(i-1),2^i]区间内的整数key个数*/
  int i;
  int totaluse;//记录所有已存在的键,包括hash和array,即table里的成员个数
  for (i=0; i<=MAXBITS; i++) nums[i] = 0;  /* 初始化所有计数区间*/
  nasize = numusearray(t, nums);  /* 以区间统计数组里不为nil的个数,并获得总数*/
  totaluse = nasize;  /* all those keys are integer keys */
  totaluse += numusehash(t, nums, &nasize);  /* 统计hash表里已有的键,以及整数键的个数已经区间分布*/
  //如果新key是整数类型的情况
  nasize += countint(ek, nums);
  //累计新key
  totaluse++;
  /* 重新计算数组空间 */
  na = computesizes(nums, &nasize);
  /* 重新创建内存空间, nasize为新数组大小
//totaluse - na表示所有键的个数减去新数组的个数,即为新hash表需要存放的个数 */
  resize(L, t, nasize, totaluse - na);
}
4.2 resize
static void resize (lua_State *L, Table *t, int nasize, int nhsize) {
  int i;
  int oldasize = t->sizearray;
  int oldhsize = t->lsizenode;
  Node *nold = t->node;  /* 保存当前的hash表,用于后面创建新hash表时,可以重新对各个node赋值*/
  if (nasize > oldasize)  /* 是否需要扩展数组 */
    setarrayvector(L, t, nasize);
  /* 重新分配hash空间*/
  setnodevector(L, t, nhsize);  
  if (nasize < oldasize) {  /* 小于之前大小,即有部分整数key放到了hash里 */
    t->sizearray = nasize;
    /* 超出部分存放到hash表里*/
    for (i=nasize; iarray[i]))
        setobjt2t(L, luaH_setnum(L, t, i+1), &t->array[i]);
    }
    /* 重新分配数组空间,去掉后面溢出部分*/
    luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
  }
  /* 从后到前遍历,把老hash表的值搬到新表中*/
  for (i = twoto(oldhsize) - 1; i >= 0; i--) {
    Node *old = nold+i;
    if (!ttisnil(gval(old)))
      setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old));
  }
  //释放老hash表空间
  if (nold != dummynode)
    luaM_freearray(L, nold, twoto(oldhsize), Node);  /* free old array */
}

你可能感兴趣的:(由浅入深的理解Lua的数据结构——table)