Lua 表(table)

介绍

表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构。使用表,Lua语言可以以一一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结构。Lua语言也使用表来表示包( package )和其他对象。当调用函数math.sin时,我们可能认为是“调用了math库中函数sin”; 而对于Lua语言来说,其实际含义是“以字符串"sin"为键检索表math”。

Lua语言中的表本质上是一种辅助数组( associative array ),这种数组不仅可以使用数值作为索引,也可以使用字符串或其他任意类型的值作为索引( nil除外)。

Lua语言中的表要么是值要么是变量,它们都是对象(object) 。如果读者对Java或Scheme中的数组比较熟悉,那么应该很容易理解上述概念。可以认为,表是一种动态分配的对象,程序只能操作指向表的引用(或指针)。除此以外,Lua语言不会进行隐藏的拷贝( hidden copies )或创建新的表。

初始化

  1. local t = {}                                     -- 创建一个空表
  2. local t = {x = 10, y = 10}               -- 创建一个包含x,y元素的表
  3. local t = {};t.x = 10; t.y = 10        -- 创建一个包含x,y元素的表

上述2/3中创建表的方式是等价的,不过在第二种写法中,由于可以提前判断表的大小,所以运行速度更快。

删除

删除表的元素只需要把对应元素赋值为nil便可。

t.x = nil

内部结构(数组和哈希)以及运行方式

一般情况下,你不需要知道Lua实现表的细节,就可以使用它。实际上,Lua花了很多功夫来隐藏内部的实现细节。但是,实现细节揭示了表操作的性能开销情况。因此,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是很有好处的。

Lua的表的实现使用了一些很聪明的算法。每个Lua表的内部包含两个部分:数组部分和哈希部分。

哈希部分使用哈希算法来保存和查找键。它使用被称为开放地址表的实现方式,意思是说所有的元素都保存在哈希数组中。用一个哈希函数来获取一个键对应的索引;如果存在冲突的话(意即,如果两个键产生了同一个哈希值),这些键将会被放入一个链表,其中每个元素对应一个数组项。当Lua需要向表中添加一个新的键,但哈希数组已满时,Lua将会重新哈希。重新哈希的第一步是决定新的数组部分和哈希部分的大小。因此,Lua遍历所有的元素,计数并对其进行归类,然后为数组部分选择一个大小,这个大小相当于能使数组部分超过一半的空间都被填满的2的最大的幂;然后为哈希部分选择一个大小,相当于正好能容纳哈希部分所有元素的2的最小的幂。

当Lua创建空表时,两个部分的大小都是0。因此,没有为其分配数组。让我们看一看当执行下面的代码时会发生什么:

local a = {}
for i = 1, 3 do
    a[i] = true
end

这段代码始于创建一个空表。在循环的第一次迭代中,赋值语句

a[1] = true

触发了一次重新哈希;Lua将数组部分的大小设为1,哈希部分依然为空;第二次迭代时

a[2] = true

触发了另一次重新哈希,将数组部分扩大为2.最终,第三次迭代又触发了一次重新哈希,将数组部分的大小扩大为4。

类似下面的代码

a = {}
a.x = 1; a.y = 2; a.z = 3

做的事情类似,只不过增加的是哈希部分的大小。

对于大的表来说,初期的几次重新哈希的开销被分摊到整个表的创建过程中,一个包含三个元素的表需要三次重新哈希,而一个有一百万个元素的表也只需要二十次。但是当创建几千个小表的时候,重新哈希带来的性能影响就会非常显著。

旧版的Lua在创建空表时会预选分配大小(4,如果我没有记错的话),以防止在初始化小表时产生的这些开销。但是这样的实现方式会浪费内存。例如,如果你要创建数百万个点(表现为包含两个元素的表),每个都使用了两倍于实际所需的内存,就会付出高昂的代价。这也是为什么Lua不再为新表预分配数组。

如果你使用C编程,可以通过Lua的API函数lua_createtable来避免重新哈希;除lua_State之外,它还接受两个参数:数组部分的初始大小和哈希部分的初始大小[1]。只要指定适当的值,就可以避免初始化时的重新哈希。需要警惕的是,Lua只会在重新哈希时收缩表的大小,因此如果在初始化时指定了过大的值,Lua可能永远不会纠正你浪费的内存空间。

当使用Lua编程时,你可能可以使用构造式来避免初始化时的重新哈希。当你写下

{true, true, true}

时,Lua知道这个表的数组部分将会有三个元素,因此会创建相应大小的数组。类似的,如果你写下

{x = 1, y = 2, z = 3}

Lua也会为哈希部分创建一个大小为4的数组。例如,执行下面的代码需要2.0秒:

for i = 1, 1000000 do
    local a = {}
    a[1] = 1; a[2] = 2; a[3] = 3
end

如果在创建表时给定正确的大小,执行时间可以缩减到0.7秒:

for i = 1, 1000000 do
    local a = {true, true, true}
    a[1] = 1; a[2] = 2; a[3] = 3
end

但是,如果你写类似于

{[1] = true, [2] = true, [3] = true}

的代码,Lua还不够聪明,无法识别表达式(在本例中是数值字面量)指定的数组索引,因此它会为哈希部分创建一个大小为4的数组,浪费内存和CPU时间。

两个部分的大小只会在Lua重新哈希时重新计算,重新哈希则只会发生在表完全填满后,Lua需要插入新的元素之时。因此,如果你遍历一个表并清除其所有项(也就是全部设为nil),表的大小不会缩小。但是此时,如果你需要插入新的元素,表的大小将会被调整。多数情况下这都不会成为问题,但是,不要指望能通过清除表项来回收内存:最好是直接把表自身清除掉。

你可能会好奇Lua为什么不会在清除表项时收缩表。首先是为了避免测试写入表中的内容。如果在赋值时检查值是否为nil,将会拖慢所有的赋值操作。第二,也是最重要的,允许在遍历表时将表项赋值为nil。例如下面的循环:

for k, v in pairs(t) do
    if some_property(v) then
        t[k] = nil – 清除元素
    end
end

如果Lua在每次nil赋值后重新哈希这张表,循环就会被破坏。

如果你想要清除一个表中的所有元素,只需要简单地遍历它:

for k in pairs(t) do
    t[k] = nil
end

一个“聪明”的替代解决方案:

while true do
    local k = next(t)
    if not k then break end
    t[k] = nil
end

但是,对于大表来说,这个循环将会非常慢。调用函数next时,如果没有给定前一个键,将会返回表的第一个元素(以某种随机的顺序)。在此例中,next将会遍历这个表,从开始寻找一个非nil元素。由于循环总是将找到的第一个元素置为nil,因此next函数将会花费越来越长的时间来寻找第一个非nil元素。这样的结果是,这个“聪明”的循环需要20秒来清除一个有100,000个元素的表,而使用pairs实现的循环则只需要0.04秒。

通过使用闭包,我们可以避免使用动态编译。下面的代码只需要十分之一的时间完成相同的工作:

function fk (k)
    return function () return k end
end

local lim = 100000
local a = {}
for i = 1, lim do a[i] = fk(i) end

print(a[10]()) --> 10

取长度

常用的取长度方式为#
而#的使用又有些需要注意的地方。
首先要明确的是lua中有两部分:数组部分和hash表部分。而基本上所有操作都是先数组后hash表。

local test1 = { 1 , 2 , 3 , 4 , 5 }
print(#test1)
打印结果: 5


local test1 = { 1, 3 , 5 , 2 , 4 }
print(#test1)
打印结果: 5 (好吧。。。。当然跟上面一样,都是作为数组中的值。。。)

local test1 = {[1] = 1 , [2] = 2 , [3] = 3 , [4] = 4 ,[5] = 5}
print(#test1)
打印结果: 5 (这里table中没有数组部分了,只有hash表部分)

local test1 = {[1] = 1 , [3] = 3 , [4] = 4 , [6] = 6 ,[2] = 2}
print(#test1)
打印结果: 6


明明写的table中只有5个元素,怎么会变成6那。。。。这里的原因就要看下lua源码实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/*

** Try to find a boundary in table `t'. A `boundary' is an integer index

** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).

*/

int luaH_getn (Table *t) {

  unsigned int j = t->sizearray;

  if (j > 0 && ttisnil(&t->array[j - 1])) {

    /* there is a boundary in the array part: (binary) search for it */

    unsigned int i = 0;

    while (j - i > 1) {

      unsigned int m = (i+j)/2;

      if (ttisnil(&t->array[m - 1])) j = m;

      else i = m;

    }

    return i;

  }

  /* else must find a boundary in hash part */

  else if (isdummy(t->node))  /* hash part is empty? */

    return j;  /* that is easy... */

  else return unbound_search(t, j);

}



还是先数组,数组没有后hash部分。再来看下关于hash表部分的取长度

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

static int unbound_search (Table *t, unsigned int j) {

  unsigned int i = j;  /* i is zero or a present index */

  j++;

  /* find `i' and `j' such that i is present and j is not */

  while (!ttisnil(luaH_getint(t, j))) {

    i = j;

    j *= 2;

    if (j > cast(unsigned int, MAX_INT)) {  /* overflow? */

      /* table was built with bad purposes: resort to linear search */

      i = 1;

      while (!ttisnil(luaH_getint(t, i))) i++;

      return i - 1;

    }

  }

  /* now do a binary search between them */

  while (j - i > 1) {

    unsigned int m = (i+j)/2;

    if (ttisnil(luaH_getint(t, m))) j = m;

    else i = m;

  }

  return i;

}

j++保证j是hash部分的第一个值,从j开始,如果j位置是有值的,那么将j扩大两倍,再检查两倍之后hash表中是否可以取到值,直到找到没有值的地方,这个值就在i 到 j这个区间中。然后再用折半查找找到 i 到 j之间找到的最后一个nil的,前面的就是它的长度了。 错略看来。luaH_getint用来取值 
const TValue *luaH_getint (Table *t, int key)而它的声明看来 ,第二个参数是key,通过key来取value, 而外面对传入的key是++的操作 可知计算长度用来寻找的这个key一定是个整形,而且还得是连续的(不一定)。(当然这个是不深究细节实现错略看下来的分析。。。。。)

遍历

ipairs遍历  从下表为1的元素开始遍历,遇到nil结束。

local t = {
    [5] = "105",
    [6] = "106",
    [8] = "108",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in ipairs(t)  do
    print(v)
end

输出:

1

2

3

4

105

106

 

pairs遍历,受限于表在lua语言中的底层实现机制,遍历过程中元素的出现顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。

local t = {
    [105] = "105",
    [106] = "106",
    [107] = "107",
    [108] = "108",
    [109] = "109",
    [110] = "110",
    [111] = "111",
    [112] = "112",
    [113] = "113",
    [114] = "114",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in pairs(t)  do
    print(v)
end

输出: 

112

113

114

3

4

2

1

105

106

107

108

109

110

111

转载自:https://blog.csdn.net/wangmanjie/article/details/52793902

https://blog.csdn.net/weixin_42973416/article/details/103294010

https://blog.csdn.net/summerhust/article/details/18599375

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值