lua table remove元素的問題

當我在工做中使用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初學者也許有些幫助(至少當初我在寫的時候在網上就找了好久,不知道是由於大牛們都認爲這些很簡單,不須要說,仍是由於我笨,連這都要問)。數組

首先要明確一點,就是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值排列的順序來遍歷的。post

 

固然,同時lua也提供了按照key的大小順序來遍歷的,注意,是大小順序,仍然不是key定義的順序,這種遍歷方式就是for k,v in ipairs(tbtest) do。測試

for k,v in ipairs(tbtest) do 這樣的循環必需要求tbtest中的key爲順序的,並且必須是從1開始,ipairs只會從1開始按連續的key順序遍歷到key不連續爲止。ui

 
  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的實現就可以更加清晰的說明這些了。

---------------------------------------------------------------------------------------------分割線-----------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------分割線-----------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------分割線-----------------------------------------------------------------------------------------------------------

 

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

相關文章
相關標籤/搜索