Zack Lee
Zack Lee

Reputation: 3044

Inserting and removing table element in Lua

I don't understand why the following code produces error.

The code begins with the main() function at the bottom.

heads = {}
function push(t)
    if (#t == 2) then
        table.insert(heads, t)
    end
end
function remove(id)
    for i = 1, #heads do
        if (heads[i][2] == id) then
            table.remove(heads, i)
        end
    end
end
function main()
    push({50, 1})
    push({50, 2})
    push({50, 3})
    remove(2)
end

When I run the code, I get attempt to index a nil value (field '?') error.

I expect to push the subtable elements into the table and then remove only the second one. So the resulting elements can be {50, 1} and {50, 3}.

Why is my code not working and how to fix this?

Upvotes: 3

Views: 13413

Answers (5)

Martin Braun
Martin Braun

Reputation: 12589

Andrew got it right. Never try to remove a value inside a table when you iterate the table. This is a common issue in many languages. Usually, you would store the value first and then remove like so:

local e
for i = 1, #heads do
    if (heads[i][2] == id) then
        e = i
        break
    end
end
if e then table.remove(heads, e) end

However, this solution is slow. Simply use the ID as key of your table:

local heads = {}

heads[1] = 50 -- push
heads[2] = 50
heads[3] = 50
heads[2] = nil -- remove

No need for unnecessary function calls and iterations.

Upvotes: 2

Vollbracht
Vollbracht

Reputation: 1

Addressing this iteration, in case of many items that are to be removed, I'd rather go for

local result = {}
for _, v in ipairs(myTab) do
  if v == 'nice' then
    table.insert(result, v)
  end
end
myTab = result

as table.remove is slow when shifting many elements.

Upvotes: 0

Curtis Fenner
Curtis Fenner

Reputation: 1403

As Andrew mentioned, for i = 1, #heads do will go to the original length of the list; if you shorten heads during the loop, then the final iteration(s) will read heads[i] and find only nil.

A simple way to fix this is to move backwards through the list, since removing an element only affects indices after the index you have removed from:

for i = #heads, 1, -1 do
    if heads[i][2] == id then
        table.remove(heads, i)
    end
end

Note that in any case, this is O(n*d) complexity and could be very slow if you are deleting many elements from the list. And, as others pointed out, there's a O(1) approach where you use a map from v[1] => v instead.

Upvotes: 1

cyclaminist
cyclaminist

Reputation: 1807

To avoid problems caused by removing fields while iterating over an array, I've used a while loop with an index variable, which is incremented at the end of each iteration, but decremented when an index is removed. So, for instance, to remove all elements with an even index:

local t = { 1, 2, 3, 4, 5 }
local i = 1
while t[i] do
  if t[i] % 2 == 0 then
    table.remove(t, i)
    i = i - 1
  end
  i = i + 1
end

This method allows you to iterate over array indices in ascending order.

Upvotes: 0

Andrew Kashpur
Andrew Kashpur

Reputation: 746

According to 5.1 manual table.remove "Removes from table the element at position, shifting down other elements to close the space, if necessary"

size of heads (#heads) is calculated once before loop execution, when i==2 you call table.remove, and so size of the table shrinks to 2, and on next iteration you try index heads[3][2], but heads[3] is nil, therefore "attempt to index a nil value" error message.

Upvotes: 3

Related Questions