Accessing deeply nested table without error?

762 views Asked by At

For a field inside a deeply nested table, for example, text.title.1.font. Even if you use

if text.title.1.font then ... end

it would result in an error like "attempt to index global 'text' (a nil value)" if any level of the table does not actually exists. Of course one may tried to check for the existence of each level of the table, but it seems rather cumbersome. I am wondering is there a safe and prettier way to handle this, such that when referencing such an object, nil would be the value instead of triggering an error?

2

There are 2 answers

0
Paul Kulchenko On

Egor's suggestion debug.setmetatable(nil, {__index = function()end}) is the easiest to apply. Keep in mind that it's not lexically scoped, so, once it's on, it will be "on" until turned off, which may have unintended consequences in other parts of your code. See this thread for the discussion and some alternatives.

Also note that text.title.1.font should probably be text.title[1].font or text.title['1'].font (and these two are not the same).

Another, a bit more verbose, but still usable alternative is:

if (((text or {}).title or {})[1] or {}).font then ... end
0
luther On

The way to do this that doesn't invite lots of bugs is to explicitly tell Lua which fields of which tables should be tables by default. You can do this with metatables. The following is an example, but it should really be customized according to how you want your tables to be structured.

-- This metatable is intended to catch bugs by keeping default tables empty.
local default_mt = {
  __newindex =
    function()
      error(
    'This is a default table. You have to make nested tables the old-fashioned way.')
    end
}

local number_mt = {
  __index =
    function(self, key)
      if type(key) == 'number' then
    return setmetatable({}, default_mt)
      end
    end
}

local default_number_mt = {
  __index = number_mt.__index,
  __newindex = default_mt.__newindex
}

local title_mt = {__index = {title = setmetatable({}, default_number_mt)}}

local text = setmetatable({}, title_mt)

print(text.title[1].font)