lua table remove元素的问题

当我在工作中使用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的实现就能够更加清晰的说明这些了。

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

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

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

原文  http://childhood.logdown.com/posts/200499/lua-table-how-to-safely-remove-an-element

在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 切记 切记

时间: 2024-08-25 18:09:46

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

Lua Table元素的删除

Lua 中的Table元素删除主要有两种方法:1.将字段赋值为 nil2.使用Table库里的 table.remove(table, index) 下面着重讲解两种方法 1 table.remove 先来看一下这个库函数的函数原型: table.remove(table, pos) @table:要删除的表@pos: 要删除表元素的位置,这个参数可选,如果不传的话, 默认为table的长度,即从表最后一个元素删起 local t = {10, 20, 30, 40,50, 60} table.

lua table integer index 特性

table.maxn (table) Returns the largest positive numerical index of the given table, or zero if the table has no positive numerical indices. (To do its job this function does a linear traversal of the whole table.) 返回表中最大的正数值index. 说明: 1. 此接口不是统计表中元素的

LUA table学习笔记

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

lua table相关注意事项

1. 判断是否为空: 使用lua内置的next函数,比如: if next(tableData) ~= nil then 但要注意:使用next判断的话,不得赋予tableData新的元素,否则结果是未知的. 2. 删除元素: 不建议方式: local datalist = {1,2,3,4,5,6,7,8,9,10,20} for k, v in pairs(datalist) do if v % 2 == 0 then table.remove(datalist, k) end end ta

lua table 的操作

table在前面作过介绍,它是一种关联数组,这种关联指的是可以设置各类类型的key来存储值. 1.table 间的数据传递 -- 为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存地址 -- 如果 a 设置为 nil ,则 b 同样能访问 table 的元素. -- 如果没有指定的变量指向a,Lua的垃圾回收机制会清理相对应的内存. mytable = {}; print("mytable的类型是:",type(mytable)); mytable

从实现求差集介绍lua table需要注意的一些问题

用lua实现的求两个table的差集(只支持一维table) 1.lua table 判空: table 判空,用的了next()函数. next()函数说明:运行程序来遍历表中的所有域. 第一个参数是要遍历的表,第二个参数是表中的某个键. next 返回该键的下一个键及其关联的值. 如果用 nil 作为 第二个参数调用 next 将返回初始键及其关联值. 当以最后一个键去调用,或是以 nil 调用一张空表时, next 返回 nil. 如果不提供第二个参数,将认为它就是 nil. 可以用 ne

LUA table

1 table实现介绍 脚本table中的元素在c中是分两个地方存放的,即数组与hash表.table中元素的位置也不是一直固定的,它会根据table被修改的情况动态改变.下面分两种情况说一下table的特点,我们首先要知道在lua中,索引值可以为负数也可以为正数,当为负数的话,top为-1,当为正数第一个压入栈的元素为1,依此类推,如果构造table的时候指定了key的值,不管key是何值,也会将key进行hash.比如 t1 = { [1] = 100, [2] = 200, [3] = 3

Lua Table 操作

Lua中table类似与C#种的字典,其实就是一个key-value键值对数据结构.来学习下table基本操作 Table的创建 myTable = {} --表名后面使用{}赋值,表示一个空的表 myTable = {name="盘子脸",age=18,isMan=true} --创建时候就添加键-值 myTable = {10,20,30,"plateface"} --创建数字下标值的table,默认是从1开始 Table的赋值 myTable[3] = 34

树形打印lua table表

local print = print local tconcat = table.concat local tinsert = table.insert local srep = string.rep local type = type local pairs = pairs local tostring = tostring local next = next function print_lua_table (lua_table, indent) if not lua_table or t