Dictionary Editing While Iterating

Is it safe to “edit” a dictionary while iterating through it? I know this is bad to do with arrays. For example, this snippet of code seems to work fine:

local hash = {
	["a"] = 1,
	["b"] = 0,
	["c"] = 0,
	["d"] = 1,
	["e"] = 1,
	["f"] = 0,
}

for i, v in pairs(hash) do
	if v == 1 then
		hash[i] = nil
	else
		print(i)
	end
end

It’s perfectly fine to do that. Why do you think it’s unsafe?

the dictionary being iterated is the original dictionary and will iterates through indexes created during the iteration period

local dictionary = {["gaming1"] = 1; ["gaming2"] = 2}

for i, v in pairs(dictionary) do
	dictionary["gaming3"] = 3
    print(i)
end

notice that this would outputs

gaming1 
gaming3 
gaming1 

this is because if you then print out every items in the dictionary, you can see the third gaming index has taken the position of the first gaming index, causing gaming2 to be skipped (as gaming1 was pushed to the position of gaming2, which is the position the script is iterating to next) and gaming1 repeated

local dictionary = {["gaming1"] = 1; ["gaming2"] = 2}

for i, v in pairs(dictionary) do
	dictionary["gaming3"] = 3
	print(i)
end 
print("----------------")
for i, v in pairs(dictionary) do
	print(i)
end 

outputs

gaming1  
gaming3 
gaming1
----------------
gaming2  
gaming3 
gaming1  

tl;dr: it’s fine to edit the values of indexes while iterating but don’t edit the indexes else things get unpredictable

2 Likes

To really understand this, it’s helpful to know why editing while iterating over an array can sometimes be an issue.

It arises when the “editing” actually means moving and/or removing elements from the array. From https://www.lua.org/pil/19.2.html:

The table.remove function removes (and returns) an element from a given position in an array, moving down other elements to close space and decrementing the size of the array. When called without a position, it removes the last element of the array.

In other words, calling table.remove(t, i) changes the index of every element after the i’th.

When happens when we remove e.g. the 1st element is that the second element is moved down into the 1st position, the 2nd to the 3rd, and so on. When the next iteration in the loop tries to do something to the “next” element, that’s the element in the 2nd position which is the 3rd element. I.e. it skips the 2nd element!

Here’s an example:

local t = {'a', 'b', 'c'}
for k, v in ipairs(t) do
    if v == 'a' or v == 'b' then
        table.remove(t, k)
    end
end
print(table.concat(t, ", "))

You’d think that it would remove any elements that are ‘a’ or ‘b’, but it doesn’t remove ‘b’ because that gets skipped. You can work around it by manually keeping track of how many elements were removed, like so:

local t = {'a', 'b', 'c'}
local offset = 0
for k = 1, #t do
    local k = k - offset
    local v = t[k]
    if v == 'a' or v == 'b' then
        table.remove(t, k)
        offset = offset + 1
    end
end
print(table.concat(t, ", "))

… or more elegantly by iterating in reverse order:

local t = {'a', 'b', 'c'}
for k = #t, 1, -1 do
    local v = t[k]
    if v == 'a' or v == 'b' then
        table.remove(t, k)
    end
end
print(table.concat(t, ", "))

The reason then that “editing while iterating” over a dict is that removing elements from a dict doesn’t shift any other elements around. You might still get issues if you do some other kind of “editing” that changes the indices of keys that haven’t been iterated over yet.

6 Likes

But for my situation it’s safe to set the index values of dictionaries while iterating, yes?