前几天在修复一个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