Lua 表(table)

介紹

表(Table)是Lua語言中最主要(事實上也是唯一的)和強大的數據結構。使用表,Lua語言可以以一一種簡單、統一且高效的方式表示數組、集合、記錄和其他很多數據結構。Lua語言也使用表來表示包( package )和其他對象。當調用函數math.sin時,我們可能認爲是“調用了math庫中函數sin”; 而對於Lua語言來說,其實際含義是“以字符串"sin"爲鍵檢索表math”。

Lua語言中的表本質上是一種輔助數組( associative array ),這種數組不僅可以使用數值作爲索引,也可以使用字符串或其他任意類型的值作爲索引( nil除外)。

Lua語言中的表要麼是值要麼是變量,它們都是對象(object) 。如果讀者對Java或Scheme中的數組比較熟悉,那麼應該很容易理解上述概念。可以認爲,表是一種動態分配的對象,程序只能操作指向表的引用(或指針)。除此以外,Lua語言不會進行隱藏的拷貝( hidden copies )或創建新的表。

初始化

  1. local t = {}                                     -- 創建一個空表
  2. local t = {x = 10, y = 10}               -- 創建一個包含x,y元素的表
  3. local t = {};t.x = 10; t.y = 10        -- 創建一個包含x,y元素的表

上述2/3中創建表的方式是等價的,不過在第二種寫法中,由於可以提前判斷表的大小,所以運行速度更快。

刪除

刪除表的元素只需要把對應元素賦值爲nil便可。

t.x = nil

內部結構(數組和哈希)以及運行方式

一般情況下,你不需要知道Lua實現表的細節,就可以使用它。實際上,Lua花了很多功夫來隱藏內部的實現細節。但是,實現細節揭示了表操作的性能開銷情況。因此,要優化使用表的程序(這裏特指Lua程序),瞭解一些表的實現細節是很有好處的。

Lua的表的實現使用了一些很聰明的算法。每個Lua表的內部包含兩個部分:數組部分和哈希部分。

哈希部分使用哈希算法來保存和查找鍵。它使用被稱爲開放地址表的實現方式,意思是說所有的元素都保存在哈希數組中。用一個哈希函數來獲取一個鍵對應的索引;如果存在衝突的話(意即,如果兩個鍵產生了同一個哈希值),這些鍵將會被放入一個鏈表,其中每個元素對應一個數組項。當Lua需要向表中添加一個新的鍵,但哈希數組已滿時,Lua將會重新哈希。重新哈希的第一步是決定新的數組部分和哈希部分的大小。因此,Lua遍歷所有的元素,計數並對其進行歸類,然後爲數組部分選擇一個大小,這個大小相當於能使數組部分超過一半的空間都被填滿的2的最大的冪;然後爲哈希部分選擇一個大小,相當於正好能容納哈希部分所有元素的2的最小的冪。

當Lua創建空表時,兩個部分的大小都是0。因此,沒有爲其分配數組。讓我們看一看當執行下面的代碼時會發生什麼:

local a = {}
for i = 1, 3 do
    a[i] = true
end

這段代碼始於創建一個空表。在循環的第一次迭代中,賦值語句

a[1] = true

觸發了一次重新哈希;Lua將數組部分的大小設爲1,哈希部分依然爲空;第二次迭代時

a[2] = true

觸發了另一次重新哈希,將數組部分擴大爲2.最終,第三次迭代又觸發了一次重新哈希,將數組部分的大小擴大爲4。

類似下面的代碼

a = {}
a.x = 1; a.y = 2; a.z = 3

做的事情類似,只不過增加的是哈希部分的大小。

對於大的表來說,初期的幾次重新哈希的開銷被分攤到整個表的創建過程中,一個包含三個元素的表需要三次重新哈希,而一個有一百萬個元素的表也只需要二十次。但是當創建幾千個小表的時候,重新哈希帶來的性能影響就會非常顯著。

舊版的Lua在創建空表時會預選分配大小(4,如果我沒有記錯的話),以防止在初始化小表時產生的這些開銷。但是這樣的實現方式會浪費內存。例如,如果你要創建數百萬個點(表現爲包含兩個元素的表),每個都使用了兩倍於實際所需的內存,就會付出高昂的代價。這也是爲什麼Lua不再爲新表預分配數組。

如果你使用C編程,可以通過Lua的API函數lua_createtable來避免重新哈希;除lua_State之外,它還接受兩個參數:數組部分的初始大小和哈希部分的初始大小[1]。只要指定適當的值,就可以避免初始化時的重新哈希。需要警惕的是,Lua只會在重新哈希時收縮表的大小,因此如果在初始化時指定了過大的值,Lua可能永遠不會糾正你浪費的內存空間。

當使用Lua編程時,你可能可以使用構造式來避免初始化時的重新哈希。當你寫下

{true, true, true}

時,Lua知道這個表的數組部分將會有三個元素,因此會創建相應大小的數組。類似的,如果你寫下

{x = 1, y = 2, z = 3}

Lua也會爲哈希部分創建一個大小爲4的數組。例如,執行下面的代碼需要2.0秒:

for i = 1, 1000000 do
    local a = {}
    a[1] = 1; a[2] = 2; a[3] = 3
end

如果在創建表時給定正確的大小,執行時間可以縮減到0.7秒:

for i = 1, 1000000 do
    local a = {true, true, true}
    a[1] = 1; a[2] = 2; a[3] = 3
end

但是,如果你寫類似於

{[1] = true, [2] = true, [3] = true}

的代碼,Lua還不夠聰明,無法識別表達式(在本例中是數值字面量)指定的數組索引,因此它會爲哈希部分創建一個大小爲4的數組,浪費內存和CPU時間。

兩個部分的大小隻會在Lua重新哈希時重新計算,重新哈希則只會發生在表完全填滿後,Lua需要插入新的元素之時。因此,如果你遍歷一個表並清除其所有項(也就是全部設爲nil),表的大小不會縮小。但是此時,如果你需要插入新的元素,表的大小將會被調整。多數情況下這都不會成爲問題,但是,不要指望能通過清除表項來回收內存:最好是直接把表自身清除掉。

你可能會好奇Lua爲什麼不會在清除表項時收縮表。首先是爲了避免測試寫入表中的內容。如果在賦值時檢查值是否爲nil,將會拖慢所有的賦值操作。第二,也是最重要的,允許在遍歷表時將表項賦值爲nil。例如下面的循環:

for k, v in pairs(t) do
    if some_property(v) then
        t[k] = nil – 清除元素
    end
end

如果Lua在每次nil賦值後重新哈希這張表,循環就會被破壞。

如果你想要清除一個表中的所有元素,只需要簡單地遍歷它:

for k in pairs(t) do
    t[k] = nil
end

一個“聰明”的替代解決方案:

while true do
    local k = next(t)
    if not k then break end
    t[k] = nil
end

但是,對於大表來說,這個循環將會非常慢。調用函數next時,如果沒有給定前一個鍵,將會返回表的第一個元素(以某種隨機的順序)。在此例中,next將會遍歷這個表,從開始尋找一個非nil元素。由於循環總是將找到的第一個元素置爲nil,因此next函數將會花費越來越長的時間來尋找第一個非nil元素。這樣的結果是,這個“聰明”的循環需要20秒來清除一個有100,000個元素的表,而使用pairs實現的循環則只需要0.04秒。

通過使用閉包,我們可以避免使用動態編譯。下面的代碼只需要十分之一的時間完成相同的工作:

function fk (k)
    return function () return k end
end

local lim = 100000
local a = {}
for i = 1, lim do a[i] = fk(i) end

print(a[10]()) --> 10

取長度

常用的取長度方式爲#
而#的使用又有些需要注意的地方。
首先要明確的是lua中有兩部分:數組部分和hash表部分。而基本上所有操作都是先數組後hash表。

local test1 = { 1 , 2 , 3 , 4 , 5 }
print(#test1)
打印結果: 5


local test1 = { 1, 3 , 5 , 2 , 4 }
print(#test1)
打印結果: 5 (好吧。。。。當然跟上面一樣,都是作爲數組中的值。。。)

local test1 = {[1] = 1 , [2] = 2 , [3] = 3 , [4] = 4 ,[5] = 5}
print(#test1)
打印結果: 5 (這裏table中沒有數組部分了,只有hash表部分)

local test1 = {[1] = 1 , [3] = 3 , [4] = 4 , [6] = 6 ,[2] = 2}
print(#test1)
打印結果: 6


明明寫的table中只有5個元素,怎麼會變成6那。。。。這裏的原因就要看下lua源碼實現

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/*

** Try to find a boundary in table `t'. A `boundary' is an integer index

** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).

*/

int luaH_getn (Table *t) {

  unsigned int j = t->sizearray;

  if (j > 0 && ttisnil(&t->array[j - 1])) {

    /* there is a boundary in the array part: (binary) search for it */

    unsigned int i = 0;

    while (j - i > 1) {

      unsigned int m = (i+j)/2;

      if (ttisnil(&t->array[m - 1])) j = m;

      else i = m;

    }

    return i;

  }

  /* else must find a boundary in hash part */

  else if (isdummy(t->node))  /* hash part is empty? */

    return j;  /* that is easy... */

  else return unbound_search(t, j);

}



還是先數組,數組沒有後hash部分。再來看下關於hash表部分的取長度

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

static int unbound_search (Table *t, unsigned int j) {

  unsigned int i = j;  /* i is zero or a present index */

  j++;

  /* find `i' and `j' such that i is present and j is not */

  while (!ttisnil(luaH_getint(t, j))) {

    i = j;

    j *= 2;

    if (j > cast(unsigned int, MAX_INT)) {  /* overflow? */

      /* table was built with bad purposes: resort to linear search */

      i = 1;

      while (!ttisnil(luaH_getint(t, i))) i++;

      return i - 1;

    }

  }

  /* now do a binary search between them */

  while (j - i > 1) {

    unsigned int m = (i+j)/2;

    if (ttisnil(luaH_getint(t, m))) j = m;

    else i = m;

  }

  return i;

}

j++保證j是hash部分的第一個值,從j開始,如果j位置是有值的,那麼將j擴大兩倍,再檢查兩倍之後hash表中是否可以取到值,直到找到沒有值的地方,這個值就在i 到 j這個區間中。然後再用折半查找找到 i 到 j之間找到的最後一個nil的,前面的就是它的長度了。 錯略看來。luaH_getint用來取值 
const TValue *luaH_getint (Table *t, int key)而它的聲明看來 ,第二個參數是key,通過key來取value, 而外面對傳入的key是++的操作 可知計算長度用來尋找的這個key一定是個整形,而且還得是連續的(不一定)。(當然這個是不深究細節實現錯略看下來的分析。。。。。)

遍歷

ipairs遍歷  從下表爲1的元素開始遍歷,遇到nil結束。

local t = {
    [5] = "105",
    [6] = "106",
    [8] = "108",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in ipairs(t)  do
    print(v)
end

輸出:

1

2

3

4

105

106

 

pairs遍歷,受限於表在lua語言中的底層實現機制,遍歷過程中元素的出現順序可能是隨機的,相同的程序在每次運行時也可能產生不同的順序。唯一可以確定的是,在遍歷的過程中每個元素會且只會出現一次。

local t = {
    [105] = "105",
    [106] = "106",
    [107] = "107",
    [108] = "108",
    [109] = "109",
    [110] = "110",
    [111] = "111",
    [112] = "112",
    [113] = "113",
    [114] = "114",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in pairs(t)  do
    print(v)
end

輸出: 

112

113

114

3

4

2

1

105

106

107

108

109

110

111

轉載自:https://blog.csdn.net/wangmanjie/article/details/52793902

https://blog.csdn.net/weixin_42973416/article/details/103294010

https://blog.csdn.net/summerhust/article/details/18599375

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章