lua table remove元素的问题

时间:2021-10-23 23:31:09

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


  1. for key, value in pairs(tbtest) do
  2. XXX
  3. end
  4. for key, value in ipairs(tbtest) do
  5. XXX
  6. end
  7. for i=1, #(tbtest) do
  8. XXX
  9. end
  10. for i=1, table.maxn(tbtest) do
  11. XXX
  12. 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这种方式:

先看效果:


  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也提供了按照key的大小顺序来遍历的,注意,是大小顺序,仍然不是key定义的顺序,这种遍历方式就是for k,v in ipairs(tbtest) do。

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


  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. tbtest = {
  8. [6] = 6,
  9. [1] = 1,
  10. [2] = 2,
  11. }
  12. 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,
  7. }

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


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

你可能会惊讶,上面这种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,
  4. }

这个代表,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的实现就能够更加清晰的说明这些了。

---------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------

在Lua中,table如何安全的移除元素这点挺重要,因为如果不小心,会没有正确的移除,造成内存泄漏。

引子

比如有些朋友常常这么做,大家看有啥问题

将test表中的偶数移除掉

local test = { 2, 3, 4, 8, 9, 100, 20, 13, 15, 7, 11}
for i, v in ipairs( test ) do
if v % 2 == 0 then
table.remove(test, i)
end
end for i, v in ipairs( test ) do
print(i .. "====" .. v)
end

打印结果:

1====3
2====8
3====9
4====20
5====13
6====15
7====7
8====11
[Finished in 0.0s]

有问题吧,20怎么还在?这就是在遍历中删除导致的。

如何做呢?

Let's get started!

local test = { 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p' }
local remove = { a = true, b = true, c = true, e = true, f = true, p = true } local function dump(table)
for k, v in pairs( table ) do
print(k)
print(v)
print("*********")
end
end

说明:一般我们不在循环中删除,在循环中删除会造成一些错误。这是可以建立一个remove表用来标记将要删除的,如上面例子,把将要删除的标记为true

方法1 从后往前删除

for i = #test, 1, -1 do
if remove[test[i]] then
table.remove(test, i)
end
end dump(test)

为什么不从前往后,朋友们可以测试,table.remove操作后,后面的元素会往前移位,这时候后续的删除索引对应的元素已经不是之前的索引对应的元素了。

方法2 while删除

local i = 1
while i <= #test do
if remove[test[i]] then
table.remove(test, i)
else
i = i + 1
end
end dump(test)

方法3 quick中提供的removeItem

function table.removeItem(list, item, removeAll)
local rmCount = 0
for i = 1, #list do
if list[i - rmCount] == item then
table.remove(list, i - rmCount)
if removeAll then
rmCount = rmCount + 1
else
break
end
end
end
end for k, v in pairs( remove ) do
table.removeItem(test, k)
end
 
下面附带一个自己些的遍历删除的例子

local bulletDel = {}--存放删除子弹元素的table

local enemyDel = {}--存放删除敌机的table

for k_bullet, v_bullet in ipairs(bullet) do

local bulletPoint = cc.p(v_bullet:getPositionX(),v_bullet:getPositionY())

for k_enemy, v_enemy in ipairs(enemy) do

local enemyRect = v_enemy:getBoundingBox()

if cc.rectContainsPoint(enemyRect,bulletPoint) then

table.insert(bulletDel,k_bullet)--这里有个小技巧,把要删除的数据的表的索引放到删除表缓存中,这样在删除就很方便了

table.insert(enemyDel,k_enemy)

end

end

end

for key,value in ipairs(bulletDel) do

bullet[value]:removeSelf()--在游戏中把子弹删除

table.remove(bullet,value)

end

for key,value in ipairs(enemyDel) do

enemy[value]:removeSelf()

table.remove(enemy,value)

end

总结一下 就是 lua 的table是以hash的形式存储 使用迭代器来遍历 也就是说 并不会按照key的顺序从前往后排列 此为坑1  删除元素的时候前面的元素删除 后面的元素会顺序前移 故不能达到删除指定元素的目的 应该从后忘前删除 也就是逆序删除 此坑2 切记 切记

lua table remove元素的问题的更多相关文章

  1. Lua table的remove函数

    [1]remove函数简介 table.remove(table, pos): 返回table数组中位于pos位置的元素,其后的元素会被前移. pos参数可选, 默认为table长度, 即从最后一个元 ...

  2. lua table integer index 特性

    table.maxn (table) Returns the largest positive numerical index of the given table, or zero if the t ...

  3. LUA table学习笔记

    function printT( ... ) for i,v in ipairs(...) do print(i,v) end end t1={} t2={} t3={} table.insert(t ...

  4. Lua table库整理(v5&period;1)

    这个库提供了表处理的通用函数. 所有函数都放在表 table. 无论何时,若一个操作需要取表的长度, 这张表必须是一个真序列. table.concat(list, [, sep, [, i , [, ...

  5. Lua table使用

    days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Th ...

  6. 【LUA table 移除操作非常慢】

    LUA的表有插入和删除两种操作.插入操作非常快,100000次操作都在0.01S左右,而删除操作在表元素大于10000时却急速变慢,测试如下: t = {} local t1= os.clock() ...

  7. Lua 学习之基础篇四&lt&semi;Lua table&lpar;表&rpar;&gt&semi;

    table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组.字典等. Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil. Lua ta ...

  8. lua table 的操作&lpar;四&rpar;

    table在前面作过介绍,它是一种关联数组,这种关联指的是可以设置各类类型的key来存储值. 1.table 间的数据传递 -- 为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b ...

  9. Lua table&lpar;表&rpar;

    table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组.字典等. Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil. Lua ta ...

随机推荐

  1. 在注册表中无Python3&period;5安装路径的情况下安装pywin32-

    当安装pywin32出现Python Version 3.5 required which was not found in the registry的时候表面注册表中没有Python3.5的安装路径 ...

  2. &lbrack;OC&rsqb;宏与const 的使用

    Tip: OS日常工作之常用宏定义大全 (摘录文档地址,感觉还不错,例子简单易懂) extern字符串常量,宏定义字符串常量,怎么选? Define与Const专题 extern字符串常量,宏定义字符 ...

  3. UNITY3D中涉及的一些数学知识

    3D中的法线是什么意思啊~ 在空间垂直于一个平面的直线:或者在一个平面里面,垂直于一条直线的直线.法线也可以指的某个方向.对于圆弧来讲,指的是它与圆心相连的直线(方向).法线:英语叫Normal Li ...

  4. 【ASP&period;NET 问题】System&period;InvalidOperationException&colon; 对象的当前状态使该操作无效 【大量表单数据提交】错误解决

    出现的问题描述: 当页面的数据量比较大时,出现异常,详细信息: System.InvalidOperationException: 对象的当前状态使该操作无效 问题的原因:出现这个异常的原因是因为微软 ...

  5. MySQL优化 - 所需了解的基础知识

    时隔一年半,期间一直想写但却觉得没有实质性的内容可记录,本文为 [高性能MySQL] 的学习日志整理分享(感兴趣建议读原书). 优化应贯穿整个产品开发周期中,开发过程中考虑一些性能问题与影响,总比出问 ...

  6. Javascript高级编程学习笔记(68)—— 事件(12)设备事件

    设备事件 随着智能手机与平板电脑的普及,为了更好地让用户与这些设备进行交互 浏览器引入了一种新的方式,而一类新的事件也应运而生,这就是设备事件 W3C从2011年开始制定关于设备事件的草案 下面将会介 ...

  7. Django-website 程序案例系列-7 创建多对多关系表

    创建多对多关系表: 方式一:一共三张表其中有一张中间表需要手工建立(建议使用第一种方式自定制程度高) class Host(models.Model): hostname = models.CharF ...

  8. spring security&plus;freemarker获取登陆用户的信息

    spring security+freemarker获取登陆用户的信息 目标页面之间获取 ${Session.SPRING_SECURITY_CONTEXT.authentication.princi ...

  9. zoj 2676 Network Wars 0-1分数规划&plus;最小割

    题目详解出自 论文 Amber-最小割模型在信息学竞赛中的应用 题目大意: 给出一个带权无向图 G = (V,E), 每条边 e属于E都有一个权值We,求一个割边集C,使得该割边集的平均边权最小,即最 ...

  10. 【C&num;小知识】C&num;中一些易混淆概念总结(五)---------继承 分类: C&num; 2014-02-06 22&colon;05 1106人阅读 评论&lpar;0&rpar; 收藏

    目录: [C#小知识]C#中一些易混淆概念总结--------数据类型存储位置,方法调用,out和ref参数的使用 [C#小知识]C#中一些易混淆概念总结(二)--------构造函数,this关键字 ...