浅析Lua中table的遍历

当我在工做中使用lua进行开发时,发如今lua中有4种方式遍历一个table,固然,从本质上来讲其实都同样,只是形式不一样,这四种方式分别是:程序员

  
  
           
  
  
  1. for key, value in pairs(tbtest) do  
  2. XXX  
  3. end 
  4.  
  5. for key, value in ipairs(tbtest) do  
  6. XXX  
  7. end 
  8.  
  9. for i=1, #(tbtest) do  
  10.     XXX  
  11. end 
  12.  
  13. for i=1, table.maxn(tbtest) do  
  14.     XXX  
  15. end 

前两种是泛型遍历,后两种是数值型遍历。固然你还会说lua的table遍历还有不少种方法啊,没错,不过最多见的这些遍历确实有必要弄清楚。算法

这四种方式各有特色,因为在工做中我几乎天天都会使用遍历table的方法,一开始也很是困惑这些方式的不一样,一段时间后才渐渐明白,这里我也是把本身的一点经验告诉你们,对跟我同样的lua初学者也许有些帮助(至少当初我在写的时候在网上就找了好久,不知道是由于大牛们都认为这些很简单,不须要说,仍是由于我笨,连这都要问)。sql

首先要明确一点,就是lua中table并不是像是C/C++中的数组同样是顺序存储的,准确来讲lua中的table更加像是C++中的map,经过Key对应存储Value,可是并不是顺序来保存key-value对,而是使用了hash的方式,这样可以更加快速的访问key对应的value,咱们也知道hash表的遍历须要使用所谓的迭代器来进行,一样,lua也有本身的迭代器,就是上面4种遍历方式中的pairs和ipairs遍历。可是lua同时提供了按照key来遍历的方式(另外两种,实质上是一种),正式由于它提供了这种按key的遍历,才形成了我一开始的困惑,我一度认为lua中关于table的遍历是按照我table定义key的顺序来的。数组

下面依次来说讲四种遍历方式,首先来看for k,v in pairs(tbtest) do这种方式:框架

先看效果:ide

  
  
           
  
  
  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [3] = 3,  
  5.     [4] = 4,  
  6.  
  7. for key, value in pairs(tbtest) do  
  8.     print(value)  
  9. end 

我认为输出应该是1,2,3,4,实际上的输出是1,2,4,3。我由于这个形成了一个bug,这是后话。函数

也就是说for k,v in pairs(tbtest) do 这样的遍历顺序并不是是tbtest中table的排列顺序,而是根据tbtest中key的hash值排列的顺序来遍历的。lua

 

固然,同时lua也提供了按照key的大小顺序来遍历的,注意,是大小顺序,仍然不是key定义的顺序,这种遍历方式就是for k,v in ipairs(tbtest) do。spa

for k,v in ipairs(tbtest) do 这样的循环必需要求tbtest中的key为顺序的,并且必须是从1开始,ipairs只会从1开始按连续的key顺序遍历到key不连续为止。ip

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. [5] = 5,  
  6.  
  7. for k,v in ipairs(tbtest) do  
  8. print(v)  
  9. end 

只会打印1,2,3。而5则不会显示。

  
  
           
  
  
  1. local tbtest = {  
  2. [2] = 2,  
  3. [3] = 3,  
  4. [5] = 5,  
  5.  
  6. for k,v in ipairs(tbtest) do  
  7. print(v)  
  8. end 

这样就一个都不会打印。

 

第三种遍历方式有一种神奇的符号'#',这个符号的做用是是获取table的长度,好比:

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(#(tbtest)) 

打印的就是3

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [6] = 6,  
  5. }  
  6. print(#(tbtest)) 

这样打印的就是2,并且和table内的定义顺序没有关系,不管你是否先定义的key为6的值,‘#’都会查找key为1的值开始。

若是table的定义是这样的:

  
  
           
  
  
  1. tbtest = {  
  2. ["a"] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5.  
  6. print(#(tbtest)) 

那么打印的就是0了。由于‘#’没有找到key为1的值。一样:

  
  
           
  
  
  1. tbtest = {  
  2. [“a”] = 1,  
  3. [“b”] = 2,  
  4. [“c”] = 3,  
  5. }  
  6. print(#(tbtest)) 

打印的也是0

因此,for i=1, #(tbtest) do这种遍历,只能遍历当tbtest中存在key为1的value时才会出现结果,并且是按照key从1开始依次递增1的顺序来遍历,找到一个递增不是1的时候就结束再也不遍历,不管后面是否仍然是顺序的key,好比:

 

table.maxn获取的只针对整数的key,字符串的key是没办法获取到的,好比:

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(table.maxn(tbtest)) 
  7.  
  8.  
  9. tbtest = {  
  10. [6] = 6,  
  11. [1] = 1,  
  12. [2] = 2,  
  13. }  
  14. print(table.maxn(tbtest)) 

这样打印的就是3和6,并且和table内的定义顺序没有关系,不管你是否先定义的key为6的值,table.maxn都会获取整数型key中的最大值。

若是table的定义是这样的:

  
  
           
  
  
  1. tbtest = {  
  2. ["a"] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(table.maxn(tbtest)) 

那么打印的就是3了。若是table是:

  
  
           
  
  
  1. tbtest = {  
  2. [“a”] = 1,  
  3. [“b”] = 2,  
  4. [“c”] = 3,  
  5. }  
  6. print(table.maxn(tbtest))  
  7. print(#(tbtest)) 

那么打印的就所有是0了。

 

 

换句话说,事实上由于lua中table的构造表达式很是灵活,在同一个table中,你能够随意定义各类你想要的内容,好比:

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. ["a"] = 4,  
  6. ["b"] = 5,  

同时因为这个灵活性,你也没有办法获取整个table的长度,其实在coding的过程当中,你会发现,你真正想要获取整个table长度的地方几乎没有,你总能采起一种很是巧妙的定义方式,把这种须要获取整个table长度的操做避免掉,好比:

  
  
           
  
  
  1. tbtest = {  
  2. tbaaa = {  
  3. [1] = 1,  
  4. [2] = 2,  
  5. [3] = 3,  
  6. },  
  7. ["a"] = 4,  
  8. ["b"] = 5,  

你可能会惊讶,上面这种table该如何遍历呢?

  
  
           
  
  
  1. for k, v in pairs(tbtest) do  
  2. print(k, v)  
  3. end 

输出是:a 4 b 5 tbaaa table:XXXXX。

由此你能够看到,其实在table中定义一个table,这个table的名字就是key,对应的内容实际上是table的地址。

固然,若是你用

  
  
           
  
  
  1. for k, v in ipairs(tbtest) do  
  2. print(k,v)  
  3. end 

来遍历的话,就什么都不会打印,由于没有key为1的值。但当你增长一个key为1的值时,ipairs只会打印那一个值,如今你明白ipairs是如何工做的吧。

既然这里谈到了遍历,就说一下目前看到的几种针对table的遍历方式:

for i=1, #tbtest do --这种方式没法遍历全部的元素,由于'#'只会获取tbtest中从key为1开始的key连续的那几个元素,若是没有key为1,那么这个循环将没法进入

for i=1, table.maxn(tbtest) do --这种方式一样没法遍历全部的元素,由于table.maxn只会获取key为整数中最大的那个数,遍历的元素实际上是查找tbtest[1]~tbtest[整数key中最大值],因此,对于string作key的元素不会去查找,并且这么查找的效率低下,由于若是你整数key中定义的最大的key是10000,然而10000如下的key没有几个,那么这么遍历会浪费不少时间,由于会从1开始直到10000每个元素都会查找一遍,实际上大多数元素都是不存在的,好比:

  
  
           
  
  
  1. tbtest = {  
  2. [1] = 1,  
  3. [10000] = 2,  
  4. }  
  5. local count = 0  
  6. for i=1, table.maxn(tbtest) do  
  7. count = count + 1  
  8. print(tbtest[i])  
  9. end  
  10. print(count

你会看到打印结果是多么的坑爹,只有1和10000是有意义的,其余的全是nil,并且count是10000。耗时很是久。通常我不这么遍历。可是有一种状况下又必须这么遍历,这个在个人工做中还真的遇到了,这是后话,等讲完了再谈。

  
  
           
  
  
  1. for k, v in pairs(tbtest) do 

这个是惟一一种能够保证遍历tbtest中每个元素的方式,别高兴的太早,这种遍历也有它自身的缺点,就是遍历的顺序不是按照tbtest定义的顺序来遍历的,这个前面讲到过,固然,对于不须要顺序遍历的用法,这个是惟一可靠的遍历方式。

  
  
           
  
  
  1. for k, v in ipairs(tbtest) do 

这个只会遍历tbtest中key为整数,并且必须从1开始的那些连续元素,若是没有1开始的key,那么这个遍历是无效的,我我的认为这种遍历方式彻底能够被改造table和for i=1, #(tbtest) do的方式来代替,由于ipairs的效果和'#'的效果,在遍历的时候是相似的,都是按照key的递增1顺序来遍历。

好,再来谈谈为何我须要使用table.maxn这种很是浪费的方式来遍历,在工做中, 我遇到一个问题,就是须要把当前的周序,转换成对应的奖励,简单来讲,就是从一个活动开始算起,每周的奖励都不是固定的,好比1~4周给一种奖励,5~8周给另外一种奖励,或者是一种排名奖励,1~8名给一种奖励,9~16名给另外一种奖励,这种状况下,我根据长久的C语言的习惯,会把table定义成这个样子:

  
  
           
  
  
  1. tbtestAward = {  
  2. [8] = 1,  
  3. [16] = 3,  

这个表明,1~8给奖励1,9~16给奖励3。这样定义的好处是奖励我只须要写一次(这里的奖励用数字作了简化,实际上奖励也是一个大的table,里面还有很是复杂的结构)。而后我就遇到一个问题,即我须要根据周序数,或者是排名序数来肯定给哪种奖励,好比当前周序数是5,那么我应该给我定义好的key为8的那一档奖励,或者当前周序数是15,那么我应该给奖励3。由此读者看出,其实我定义的key是一个分界,小于这个key而大于上一个key,那么就给这个key的奖励,这就是我判断的条件。逻辑上没有问题,可是lua的遍历方式却把我狠狠地坑了一把。读者能够本身想想我上面介绍的4种遍历方式,该用哪种来实现个人这种需求呢?这个函数的大体框架以下:

  
  
           
  
  
  1. function GetAward(nSeq)  
  2. for 遍历整个奖励表 do  
  3. if 知足key的条件 then  
  4. return 返回对应奖励的key  
  5. end  
  6. end  
  7. return nil  
  8. end 

我也不卖关子了,分别来讲一说吧,首先由于个人key不是连续的,并且没有key为1的值,因此ipairs和'#'遍历是没用的。这种状况下理想的遍历貌似是pairs,由于它会遍历个人每个元素,可是读者不要忘记了,pairs遍历并不是是按照我定义的顺序来遍历,若是我真的使用的条件是:序数nSeq小于这个key而大于上一个key,那么就返回这个key。那么我没法保证程序执行的正确性,由于key的顺序有多是乱的,也就是有可能先遍历到的是key为16的值,而后才是key为8的值。

这么看来我只剩下table.maxn这么一种方式了,因而我写下了这种代码:

  
  
           
  
  
  1. for i=1, table.maxn(tbtestAward) do  
  2. if tbtestAward[i] ~= nil then  
  3. if nSeq <= i then  
  4. return i  
  5. end  
  6. end  
  7. end  

这么写效率确实低下,由于实际上仍是遍历了从key为1开始直到key为table.maxn中间的每个值,不过可以知足我上面的要求。当时我是这么实现的,由于这个奖励表会不断的发生变化,这样我每次修改只须要修改这个奖励表就可以知足要求了,后来我想了想,以为其实我若是本身再定义一个序数转换成对应的奖励数种类的表就能够避免这种坑爹的操做了,不过若是奖励发生修改,我须要统一排查的地方就不止这个奖励表了,权衡再三,我仍是没有改,就这么写了。没办法,不断变化的需求已经把我磨练的忘记了程序的最高理想。我甚至愿意牺牲算法的效率而去追求改动的稳定性。在此哀悼程序员的无奈。我这种时间换空间的作法确实不知道好很差。

后来我在《Programming In Lua》中看到了一个神奇的迭代器,使用它就能够达到我想要的这种遍历方式,并且不须要去遍历那些不存在的key。它的方法是把你所须要遍历的table里的key按照遍历顺序放到另外一个临时的table中去,这样只须要遍历这个临时的table按顺序取出原table中的key就能够了。以下:

首先定义一个迭代器:

  
  
           
  
  
  1. function pairsByKeys(t)  
  2.     local a = {}  
  3.     for n in pairs(t) do  
  4.         a[#a+1] = n  
  5.     end  
  6.     table.sort(a)  
  7.     local i = 0  
  8.     return function()  
  9.         i = i + 1  
  10.         return a[i], t[a[i]]  
  11.     end  
  12. end 

而后在遍历的时候使用这个迭代器就能够了,table同上,遍历以下:

  
  
           
  
  
  1. for key, value in pairsByKeys(tbtestAward) do  
  2.   if nSeq <= key then  
  3. return key  
  4. end  
  5. end 

而且后来我发现有了这个迭代器,我根本不须要先作一步获取是哪一档次的奖励的操做,直接使用这个迭代器进行发奖就能够了。大师就是大师,我怎么就没想到呢!

还有些话我尚未说,好比上面数值型遍历也并不是是像看起来那样进行遍历的,好比下面的遍历:

  
  
           
  
  
  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [3] = 3,  
  5.     [5] = 5,  
  6.  
  7. for i=1, #(tbtest) do  
  8.     print(tbtest[i])  
  9. end 

打印的顺序是:1,2,3。不会打印5,由于5已经不在table的数组数据块中了,我估计是被放到了hash数据块中,可是当我修改其中的一些key时,好比:

  
  
           
  
  
  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [4] = 4,  
  5.     [5] = 5,  
  6.  
  7. for i=1, #(tbtest) do  
  8.     print(tbtest[i])  
  9. end 

打印的内容倒是:1,2,nil,4,5。这个地方又遍历到了中间没有的key值,而且还能继续遍历下去。我最近正在看lua源码中table的实现部分,已经明白了是怎么回事,不过我想等我可以更加清晰的阐述lua中table的实现过程了再向你们介绍。用我师傅的话说就是不要使用一些未定义的行为方法,避免在工做中出错,不过工做外,我仍是但愿能明白未定义的行为中那些必然性,o(︶︿︶)o 唉!因果论的孩子伤不起。等我下一篇博文分析lua源码中table的实现就可以更加清晰的说明这些了。

相关文章
相关标签/搜索