前幾天在修復一個bug的時候發現代碼中使用了泛型for+ipairs()+table.remove()刪除元素,毫無疑問,這是一種錯誤的作法,但由於歷史配置內容緣由,致使這個BUG在以前一直沒表現出來。lua中,在for循環中調用函數ipairs時,ipairs會返回3個值,迭代函數、不可變狀態表、初始控制變量0,for的每次調用,都會把狀態表和控制變量傳入迭代函數,調用迭代函數,把控制變量+1,再獲取狀態表中相應元素,並把二者返回,直至遇到nil結束;而函數table.remove除了刪除、返回指定序列上的元素,還會把後面的元素往前移動。故而知,當表中相鄰的兩個元素都須要刪除的時候,就會發生刪除不乾淨的狀況。表是確定須要遍歷一遍的,如今問題就是,該以怎樣一種方式去遍歷表,達到正確刪除元素的目的。首先,若是是無序表,直接把對應位置上的元素置nil;若是是有序表,比較經常使用的方法有如下3種:算法
1 local a = {1, 1, 2, 3, 4, 4} 2 for i = #a, 1, -1 do
3 if a % 2 == 0 then
4 table.remove(a, i) 5 end
6 end
1 local a = {1, 1, 2, 3, 4, 4} 2 local index = 1
3 local v = a[index] 4 while v do
5 if v % 2 == 0 then
6 table.remove(a, index) 7 else
8 index = index + 1
9 end
10
11 v = a[index] 12 end
1 local a = {1, 1, 2, 3, 4, 4} 2 local tmp = {} 3 for k, v in ipairs(a) do
4 if v % 2 == 0 then
5 table.insert(tmp, v) 6 end
7 end
8 a = tmp
這3種方式的並且確均可以實現對錶元素的刪除,但好與很差真的值得商榷。或許事情還能夠進一步思考和優化。回到table.remove()自己,上面說到,table.remove()刪除位置上的元素後還會把此位置後面的元素往前移,這裏涉及到了一個效率問題。若是是有序表,在某個時刻僅須要刪除一個位置上的元素且繼續保持有序,table.remove()是必然選擇,可是,若是須要遍歷表刪除,選擇table.remove()是否仍是正確的作法?答案確定是否認的,每一次remove,都移動了大量元素,而loop+table.remove(),明顯作了不少沒有意義的工做。那上面的方式3是否就成了最佳選擇,答案也是否認。由於表的構造也須要時間,雖然基本能夠忽略不計,但更重要的是,若是由於原表較大,觸發了新表的luaH_resize,即內存從新分配,這開銷就變的大了,在c中,內存申請這方面一直是性能短板。那應該以怎樣一種方式更好地實現上面loop+table.remove()?數組
咱們知道,在lua中回收一個元素,直接把它置nil便可。因而,對於有序表,能夠得出如下實現:ide
1 local a = {1, 1, 2, 3, 4, 4} 2 local index, r_index, length = 1, 1, #a 3 while index <= length do
4 local v = a[index] 5 a[index] = nil
6 if not (v % 2 == 0) then
7 a[r_index] = v 8 r_index = r_index + 1
9 end
10
11 index = index + 1
12 end
封裝一下:函數
1 local tb = {} 2
3 function tb.remove(obj, rm_func) 4 if type(obj) ~= "table" or type(rm_func) ~= "function" then
5 return
6 end
7
8 local index, r_index, length = 1, 1, #obj 9 while index <= length do
10 local v = obj[index] 11 obj[index] = nil
12 if not rm_func(v) then
13 obj[r_index] = v 14 r_index = r_index + 1
15 end
16
17 index = index + 1
18 end
19 end
20
21 return tb
到這裏,咱們彷彿已經經過Achieve2實現了一種更好的方案來取替loop+table.remove(),下面附上測試代碼來看看運行時間對比:oop
1 local tb = {} 2
3 function tb.remove(obj, rm_func) 4 if type(obj) ~= "table" or type(rm_func) ~= "function" then
5 return
6 end
7
8 local index, r_index, length = 1, 1, #obj 9 while index <= length do
10 local v = obj[index] 11 obj[index] = nil
12 if not rm_func(v) then
13 obj[r_index] = v 14 r_index = r_index + 1
15 end
16
17 index = index + 1
18 end
19 end
20
21 local a = {} 22 local b = {} 23 local c = {} 24 local d = {} 25 local length = 1024*128
26 for i = 1, length do
27 a[i] = i 28 b[i] = i 29 c[i] = i 30 d[i] = i 31 end
32
33 rm_func = function(value) 34 return value % 2 == 0
35 end
36 local start_time = os.clock() 37
38 --test-(1.數值型for從後往前遍歷)
39 for i = #a, 1, -1 do
40 if rm_func(a[i]) then
41 table.remove(a, i) 42 end
43 end
44 print("1.數值型for從後往前遍歷 time:", os.clock() - start_time) 45
46 --test-(2.while控制下標方式)
47 start_time = os.clock() 48 local index = 1
49 local v = b[index] 50 while v do
51 if rm_func(v) then
52 table.remove(b, index) 53 else
54 index = index + 1
55 end
56
57 v = b[index] 58 end
59 print("2.while控制下標方式 time:", os.clock() - start_time) 60
61 --test-(3.構建臨時表方式)
62 start_time = os.clock() 63 local tmp = {} 64 for k, v in ipairs(c) do
65 if v % 2 == 0 then
66 table.insert(tmp, v) 67 end
68 end
69 c = tmp 70 print("3.構建臨時表方式 time:", os.clock() - start_time) 71
72 --test-(tb.remove)
73 start_time = os.clock() 74 tb.remove(d, rm_func) 75 print("tb.remove time:", os.clock() - start_time)
看到輸出,已經百分比肯定Achieve2是更好的作法,即便是經過構建臨時表的方式3依然比Achieve2差了9倍左右,並且毫無疑問方式3內存佔用更高;固然,實際測試時間還受到具體環境影響,可是Achieve2比loop+table.remove()效率更高已經能夠蓋棺定論,由此能夠想到,loop+table.remove()自己就是一個極其不合格的作法,是一個誤區,table.remove()的設計初衷確定不是讓你在loop中使用。性能
文章到這裏已經明確Achieve2是更好的替代方案,可是否可讓Achieve2變得更健全一點?若是表是無序表,或者表既有無序部分也有有序部分,當中的元素都須要遍歷判斷刪除怎麼辦,上述的Achieve2只能用於有序表,就是表的元素只存在於table的數組部分,哈希部分爲空的狀況下,那麼是否能進一步更改Achieve2的實現使適用於全部狀況?首先,要明確的是,lua提供的運算符和表標準庫函數都只能獲取表數組部分長度,對於整體長度,是獲取不了的,因此不在事先遍歷一次表的狀況下沒法判斷一個表究竟是有序表仍是無序表;其次,要使Achieve2適用全部狀況,必須使用pairs()遍歷,使用pairs()遍歷,具體算法是否依然能把Achieve2的時間複雜度控制在O(n)?測試
詳細瞭解一下函數pairs機制,函數pairs在調用的時候會返回lua的一個基本函數next和不可變狀態表t,調用next(t, key)時,該函數會以隨機次序返回表中的下一個key及其對應的值,調用next(t, nil)時,返回表中的第一個鍵值對,全部元素遍歷完時,函數next返回nil,for循環老是會把表達式列表的結果調整爲3個值,因此,在調用pairs()的時候,獲得的至關於next, t, nil;next函數的底層lua實現lua_next中,老是先遍歷數組部分,再遍歷哈希部分,基於pairs的這些具體行爲,咱們能夠獲得Achieve3:優化
1 local tb = {} 2
3 function tb.remove(obj, rm_func) 4 if type(obj) ~= "table" or type(rm_func) ~= "function" then
5 return
6 end
7
8 local r_index, length = 1, #obj 9 for k, v in pairs(obj) do --lua5.3能夠經過math.type(k)判斷
10 if type(k) == "number" and math.floor(k) == k and k > 0 and k <= length then
11 local tmp = v 12 obj[k] = nil
13 if not rm_func(tmp) then
14 obj[r_index] = tmp 15 r_index = r_index + 1
16 end
17 else
18 if rm_func(v) then
19 obj[k] = nil
20 end
21 end
22 end
23 end
24
25 return tb
Achieve3好像是個不錯的實現,可是看着那一堆判斷就頭疼,那還有沒有更好的實現方法,有,就是先遍歷表數組部分,而後改寫pairs行爲,使只遍歷哈希部分,下面是Achieve4:lua
1 local tb = {} 2
3 function tb.remove(obj, rm_func) 4 if type(obj) ~= "table" or type(rm_func) ~= "function" then
5 return
6 end
7
8 local index, r_index, length = 1, 1, #obj 9 while index <= length do
10 local v = obj[index] 11 obj[index] = nil
12 if not rm_func(v) then
13 obj[r_index] = v 14 r_index = r_index + 1
15 end
16
17 index = index + 1
18 end
19
20 local function _pairs(tb) 21 if length == 0 then
22 return next, tb, nil
23 else
24 return next, tb, length 25 end
26 end
27
28 for k, v in _pairs(obj) do
29 if rm_func(v) then
30 obj[k] = nil
31 end
32 end
33 end
34
35 return tb
可是到此咱們依然面臨着一種具體狀況,那就是咱們但願在表的有序部分刪除元素後,後面的元素保持不動,因而再度改寫Achieve4,獲得Achieve5:spa
1 local tb = {} 2
3 function tb.remove(obj, rm_func, to_sequence) 4 if type(obj) ~= "table" or type(rm_func) ~= "function" then
5 return
6 end
7
8 local length = 0
9 if to_sequence then
10 length = #obj 11 local index, r_index = 1, 1
12 while index <= length do
13 local v = obj[index] 14 obj[index] = nil
15 if not rm_func(v) then
16 obj[r_index] = v 17 r_index = r_index + 1
18 end
19
20 index = index + 1
21 end
22 end
23
24 local function _pairs(tb) 25 if length == 0 then
26 return next, tb, nil
27 else
28 return next, tb, length 29 end
30 end
31
32 for k, v in _pairs(obj) do
33 if rm_func(v) then
34 obj[k] = nil
35 end
36 end
37 end
38
39 return tb
好了,到此文章已經差很少結束了,上面的內容純屬原創都是本人在工做中的實際看法,水平有限,這也是本人的第一次更博,之後還會繼續把本身的經驗分享出來,若是有錯誤的地方還請不吝指出。下面附上本人寫的一些拓展table函數(主要用於我的測試):
1 local tb = {} 2
3 --to_sequence可選且只會在表有序部分起做用,時間複雜度O(n)
4 function tb.remove(obj, rm_func, to_sequence) 5 if type(obj) ~= "table" or type(rm_func) ~= "function" then
6 return
7 end
8
9 local length = 0
10 if to_sequence then
11 length = #obj 12 local index, r_index = 1, 1
13 while index <= length do
14 local v = obj[index] 15 obj[index] = nil
16 if not rm_func(v) then
17 obj[r_index] = v 18 r_index = r_index + 1
19 end
20
21 index = index + 1
22 end
23 end
24
25 local function _pairs(tb) --lua_next實現是迭代完數組部分再迭代哈希部分
26 if length == 0 then
27 return next, tb, nil
28 else
29 return next, tb, length 30 end
31 end
32
33 for k, v in _pairs(obj) do
34 if rm_func(v) then
35 obj[k] = nil
36 end
37 end
38 end
39
40 function tb.duplicate(obj) 41 local tb_note = {} --使指向行爲與原表一致
42 local copy_func 43 copy_func = function(obj) 44 if type(obj) ~= "table" then
45 return obj 46 elseif tb_note[obj] then
47 return tb_note[obj] 48 end
49
50 local dup = {} 51 tb_note[obj] = dup 52 for k, v in pairs(obj) do
53 dup[copy_func(k)] = copy_func(v) 54 end
55 setmetatable(dup, getmetatable(obj)) 56 return dup 57 end
58
59 return copy_func(obj) 60 end
61
62 function tb.tostring(obj) 63 local tb_note = {} --防止相同錶轉換屢次
64 local function serialize(value) 65 if tb_note[value] then
66 return tb_note[value] 67 elseif type(value) == "number" then
68 return string.format("%d", value) --%a
69 elseif type(value) == "string" then
70 return string.format("%q", value) --5.3.3版本後可使用%q轉化number,nil,boolean
71 elseif type(value) == "table" then
72 local str = "{"
73 local index_tb = tb.getsortedindexlist(value) 74 for _, v in ipairs(index_tb) do
75 str = str.."["..serialize(v).."]="..serialize(value[v])..","
76 end
77 local mt = getmetatable(value) 78 if mt then str = str.."[\"metatable\"]="..serialize(mt).."," end
79 str = str.."}"
80
81 tb_note[value] = str 82 return str 83 else
84 return tostring(value) 85 end
86 end
87
88 return serialize(obj) 89 end
90
91 local type_value = {["number"] = 1, ["string"] = 2, ["userdata"] = 3, ["function"] = 4, ["table"] = 5} 92 function tb.getsortedindexlist(obj) 93 local index_tb = {} 94 for k in pairs(obj) do
95 table.insert(index_tb, k) 96 end
97
98 local sort_func = function(a, b) 99 local type_a = type_value[type(a)] 100 local type_b = type_value[type(b)] 101 if type_a ~= type_b then
102 return type_a < type_b 103 else
104 if type_a == 1 or type_a == 2 then
105 return a < b 106 elseif type_a == 5 then
107 return tb.getlen(a) < tb.getlen(b) 108 else
109 return false
110 end
111 end
112 end
113
114 table.sort(index_tb, function(a, b) return sort_func(a, b) end) 115 return index_tb 116 end
117
118 function tb.getlen(obj) 119 local count = 0
120 for k, v in pairs(obj) do
121 count = count + 1
122 end
123
124 return count 125 end
126
127 function tb.show(obj) 128 for k, v in pairs(obj) do
129 print(k, v) 130 end
131 local mt = getmetatable(obj) 132 if mt then
133 print("metatable", mt) 134 end
135 end
136
137 return tb