Lua中table的遍历详解

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

代码如下:

for key, value in pairs(tbtest) do 
    XXX 
end
 
for key, value in ipairs(tbtest) do 
    XXX 
end
 
for i=1, #(tbtest) do 
    XXX 
end
 
for i=1, table.maxn(tbtest) do 
    XXX 
end

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

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

首先要明确一点,就是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这种方式:
先看效果:

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [3] = 3, 
    [4] = 4, 
}
 
for key, value in pairs(tbtest) do 
    print(value) 
end

我认为输出应该是1,2,3,4,实际上的输出是1,2,4,3。我因为这个造成了一个bug,这是后话。

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

当然,同时lua也提供了按照key的大小顺序来遍历的,注意,是大小顺序,仍然不是key定义的顺序,这种遍历方式就是for k,v in ipairs(tbtest) do。
for k,v in ipairs(tbtest) do 这样的循环必须要求tbtest中的key为顺序的,而且必须是从1开始,ipairs只会从1开始按连续的key顺序遍历到key不连续为止。

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [3] = 3, 
    [5] = 5, 
}
 
for k,v in ipairs(tbtest) do 
    print(v) 
end

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

代码如下:

local tbtest = { 
    [2] = 2, 
    [3] = 3, 
    [5] = 5, 
}
 
for k,v in ipairs(tbtest) do 
    print(v) 
end

这样就一个都不会打印。
 
第三种遍历方式有一种神奇的符号'#',这个符号的作用是是获取table的长度,比如:

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [3] = 3, 

print(#(tbtest))

打印的就是3

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [6] = 6, 

print(#(tbtest))

这样打印的就是2,而且和table内的定义顺序没有关系,无论你是否先定义的key为6的值,‘#'都会查找key为1的值开始。
如果table的定义是这样的:

代码如下:

tbtest = { 
    ["a"] = 1, 
    [2] = 2, 
    [3] = 3, 
}
 
print(#(tbtest))

那么打印的就是0了。因为‘#'没有找到key为1的值。同样:

代码如下:

tbtest = { 
    [“a”] = 1, 
    [“b”] = 2, 
    [“c”] = 3, 

print(#(tbtest))

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

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

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [3] = 3, 

print(table.maxn(tbtest))
 
 
tbtest = { 
    [6] = 6, 
    [1] = 1, 
    [2] = 2, 

print(table.maxn(tbtest))

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

代码如下:

tbtest = { 
    ["a"] = 1, 
    [2] = 2, 
    [3] = 3, 

print(table.maxn(tbtest))

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

代码如下:

tbtest = { 
    [“a”] = 1, 
    [“b”] = 2, 
    [“c”] = 3, 

print(table.maxn(tbtest)) 
print(#(tbtest))

那么打印的就全部是0了。

换句话说,事实上因为lua中table的构造表达式非常灵活,在同一个table中,你可以随意定义各种你想要的内容,比如:

代码如下:

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

同时由于这个灵活性,你也没有办法获取整个table的长度,其实在coding的过程中,你会发现,你真正想要获取整个table长度的地方几乎没有,你总能采取一种非常巧妙的定义方式,把这种需要获取整个table长度的操作避免掉,比如:

代码如下:

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

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

代码如下:

for k, v in pairs(tbtest) do 
    print(k, v) 
end

输出是:a 4 b 5 tbaaa table:XXXXX。
由此你可以看到,其实在table中定义一个table,这个table的名字就是key,对应的内容其实是table的地址。
当然,如果你用

代码如下:

for k, v in ipairs(tbtest) do 
    print(k,v) 
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每一个元素都会查找一遍,实际上大多数元素都是不存在的,比如:

代码如下:

tbtest = { 
    [1] = 1, 
    [10000] = 2, 

local count = 0 
for i=1, table.maxn(tbtest) do 
    count = count + 1 
    print(tbtest[i]) 
end 
print(count)

你会看到打印结果是多么的坑爹,只有1和10000是有意义的,其他的全是nil,而且count是10000。耗时非常久。一般我不这么遍历。但是有一种情况下又必须这么遍历,这个在我的工作中还真的遇到了,这是后话,等讲完了再谈。

代码如下:

for k, v in pairs(tbtest) do

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

代码如下:

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定义成这个样子:

代码如下:

tbtestAward = { 
    [8] = 1, 
    [16] = 3, 
}

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

代码如下:

function GetAward(nSeq) 
    for 遍历整个奖励表 do 
        if 满足key的条件 then 
            return 返回对应奖励的key 
        end 
    end 
    return nil 
end

我也不卖关子了,分别来说一说吧,首先因为我的key不是连续的,而且没有key为1的值,所以ipairs和'#'遍历是没用的。这种情况下理想的遍历貌似是pairs,因为它会遍历我的每一个元素,但是读者不要忘记了,pairs遍历并非是按照我定义的顺序来遍历,如果我真的使用的条件是:序数nSeq小于这个key而大于上一个key,那么就返回这个key。那么我无法保证程序执行的正确性,因为key的顺序有可能是乱的,也就是有可能先遍历到的是key为16的值,然后才是key为8的值。
这么看来我只剩下table.maxn这么一种方式了,于是我写下了这种代码:

代码如下:

for i=1, table.maxn(tbtestAward) do 
    if tbtestAward[i] ~= nil then 
        if nSeq <= i then 
            return i 
        end 
    end 
end

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

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

首先定义一个迭代器:

代码如下:

function pairsByKeys(t) 
    local a = {} 
    for n in pairs(t) do 
        a[#a+1] = n 
    end 
    table.sort(a) 
    local i = 0 
    return function() 
        i = i + 1 
        return a[i], t[a[i]] 
    end 
end

然后在遍历的时候使用这个迭代器就可以了,table同上,遍历如下:

代码如下:

for key, value in pairsByKeys(tbtestAward) do 
    if nSeq <= key then 
        return key 
    end 
end

并且后来我发现有了这个迭代器,我根本不需要先做一步获取是哪一档次的奖励的操作,直接使用这个迭代器进行发奖就可以了。大师就是大师,我怎么就没想到呢!
还有些话我还没有说,比如上面数值型遍历也并非是像看起来那样进行遍历的,比如下面的遍历:

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [3] = 3, 
    [5] = 5, 
}
 
for i=1, #(tbtest) do 
    print(tbtest[i]) 
end

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

代码如下:

tbtest = { 
    [1] = 1, 
    [2] = 2, 
    [4] = 4, 
    [5] = 5, 
}
 
for i=1, #(tbtest) do 
    print(tbtest[i]) 
end

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

(0)

相关推荐

  • c++遍历lua table示例

    c/c++从栈上获取Lua压栈过来的table数据 复制代码 代码如下: map<string,string> traverse_table(lua_State *L, int index){ map<string,string> data;    lua_pushnil(L);     // 现在的栈:-1 => nil; index => table index = index - 1;    while (lua_next(L, index))    {     

  • Lua 学习笔记之C API 遍历 Table实现代码

    Lua 通过一个虚拟栈与 C 的交互,正数索引自底向上取值,负数索引自顶向下取值. Lua 中的 Table(表)结构可以使用任何数据作为 key 进行取值.使用 C API 访问 Table 中的元素有两种方法: 复制代码 代码如下: lua_getglobal(L, t); lua_pushinteger(L, k); -- 这里可以换成其它类型的 lua_pushXXXX(L, k) 压数据到栈顶作key lua_gettable(L, -2); lua_getglobal(L, t);

  • Lua中遍历数组和table的4种方法

    方法一,可以用for来遍历: 复制代码 代码如下: do      table_week = {      "w",      "e",      "r",      "t",      "y",      "u",      "i",      }        for i = 1, #table_week do          print(table_week[

  • Lua中table的遍历详解

    当我在工作中使用lua进行开发时,发现在lua中有4种方式遍历一个table,当然,从本质上来说其实都一样,只是形式不同,这四种方式分别是: 复制代码 代码如下: for key, value in pairs(tbtest) do      XXX  end   for key, value in ipairs(tbtest) do      XXX  end   for i=1, #(tbtest) do      XXX  end   for i=1, table.maxn(tbtest)

  • java非递归实现之二叉树的前中后序遍历详解

    二叉树的前中后序遍历 核心思想:用栈来实现对节点的存储.一边遍历,一边将节点入栈,在需要时将节点从栈中取出来并遍历该节点的左子树或者右子树,重复上述过程,当栈为空时,遍历完成. 前序遍历 //非递归 //根 左 右 class Solution { public List<Integer> preorderTraversal(TreeNode root) { //用数组来存储前序遍历结果 List<Integer> list = new ArrayList<>(); i

  • 二叉树递归迭代及morris层序前中后序遍历详解

    目录 分析二叉树的前序,中序,后序的遍历步骤 1.层序遍历 方法一:广度优先搜索 方法二:递归 2.前序遍历 3.中序遍历 4.后序遍历 递归解法 前序遍历--递归 迭代解法 前序遍历--迭代 核心思想: 三种迭代解法的总结: Morris遍历 morris--前序遍历 morris--中序遍历 morris--后序遍历: 分析二叉树的前序,中序,后序的遍历步骤 1.层序遍历 方法一:广度优先搜索   (以下解释来自leetcode官方题解) 我们可以用广度优先搜索解决这个问题. 我们可以想到最

  • Lua中的面向对象编程详解

    简单说说Lua中的面向对象 Lua中的table就是一种对象,看以下一段简单的代码: 复制代码 代码如下: local tb1 = {a = 1, b = 2} local tb2 = {a = 1, b = 2} local tb3 = tb1   if tb1 == tb2 then      print("tb1 == tb2") else      print("tb1 ~= tb2") end   tb3.a = 3 print(tb1.a) 上述代码会输

  • Lua中的__index方法详解

    当我们访问一个表的不存在的域,返回结果为nil,这是正确的,但并不一定正确.实际上,这种访问触发lua解释器去查找__index metamethod:如果不存在,返回结果为nil:如果存在则由__index metamethod返回结果. 这个例子的原型是一种继承.假设我们想创建一些表来描述窗口.每一个表必须描述窗口的一些参数,比如:位置,大小,颜色风格等等.所有的这些参数都有默认的值,当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口.第一种方法是,实现一个表的构造器,对

  • Lua中的协同程序详解

    前言 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈.局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.从概念上讲,线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行.就是说,一个具有多个协同程序的程序在任意时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显式地要求挂起时,它的执行才会暂停. 协同程序基础 Lua将所有关于协同程序的函数放置在一个名为"coroutine"的table中.函数c

  • lua中赋值类型代码详解

    我们来看看lua vm在解析下面源码并生成bytecode时的整个过程: foo = "bar" local a, b = "a", "b" foo = a 首先我们先使用ChunkySpy这个工具来看看vm最终会具体生成什么样的vm instructions 在这里,开头为[数字]的行是vm真正生成的字节码,我们看到一共生成了六行字节码.首先loadk将常量表中下标为1的常量即"bar"赋给寄存器0:然后setglobal将

  • JS中的二叉树遍历详解

    二叉树是由根节点,左子树,右子树组成,左子树和友子树分别是一个二叉树. 这篇文章主要在JS中实现二叉树的遍历. 一个二叉树的例子 var tree = { value: 1, left: { value: 2, left: { value: 4 } }, right: { value: 3, left: { value: 5, left: { value: 7 }, right: { value: 8 } }, right: { value: 6 } } } 广度优先遍历 广度优先遍历是从二叉树

  • Lua中的逻辑运算符使用详解

    下表列出了所有的Lua语言支持的逻辑运算符.假设变量A持有true,而变量B持有false:  示例 试试下面的例子就明白了所有的Lua编程语言提供的逻辑运算符: 复制代码 代码如下: a = 5 b = 20 if ( a and b ) then    print("Line 1 - Condition is true" ) end if ( a or b ) then    print("Line 2 - Condition is true" ) end --

  • c# WPF中的TreeView使用详解

    在wpf中实现treeview的功能,可能看到很多分享的都是简单的绑定,仅此记录自己完成的功能. 前台 <TreeView x:Name="chapterTree" Grid.Column="0" SelectedItemChanged="chapterTree_SelectedItemChanged" PreviewMouseRightButtonDown="chapterTree_PreviewMouseRightButton

随机推荐