(I’m aware starmaq also made a post on metatables, I’ve tried to take my own twist on addressing the problem)
Introduction
Metatables, oh boy. The thing that makes tables suddenly work like magic.
So, how do they work? Sit tight, I’m taking you through a guide from a half-educated guy who cares
way too much about Lua.
I’ll be making some less used terms when talking here, so here’s a short glossary
metatable - a table containing metamethods
hashmap - a table with non-numeric keys
Metatables. How do they even?
So, I’m sure you’re aware of setmetatable
and getmetatable
. These are cool functions, I guess. It’s worth noting that a table and a metatable aren’t the same table. Though they are related.
A metatable is a sort of backend table that the base table doesn’t have, but Lua itself knows exist.
Consider the following:
local m = {foo2 = "bar1"}
local h = setmetatable({foo = "bar"}, {__index = m})
h.__index --> nil
To the interpreter (as far as the programmer is concerned), the ‘h’ table doesn’t have the metatable publically accessible, the only way to get it would be to use getmetatable
, where you could then index the __index metamethod.
setmetatable
, as seen above, allows you to merge a hashmap, with a metatable, to tell Lua that ‘hey, this table should now index m’
What metamethods have we got to our disposal.
I’ll start with the two most commonly used ones, __index
and __newindex
.
__index fires when the table is indexed, go figure. But it set it to another table. Lua suddenly says "oh so this table does have key ‘foo’, does the __index in the metatable have a key named ‘foo’. If it cant find one in the __index it will return nil.
local table1 = {a = 1}
local table2 = setmetatable({b = 2}, {__index = table1})
local table3 = setmetatable({a = 3}, {__index = table1})
print(table1.a) --> 1 (no __index, so it's a direct reference)
print(table2.a) --> 1 (key 'a' is not found in this table, but has an __index metamethod to table1, so it indexes table1)
print(table3.a) --> 3 (table has it's own 'a' key, so it returns 3, completely ignoring the __index metamethod in this case)
__index
metamethods can also be stacked, so table1 could index table2 which indexes table3.
__index
also allows for an implementation of inheritance in OOP, however this is common shunned upon
__newindex
is a metamethod that fires when a new key is added to a table, not pre-existing keys. This method also stops Lua automatically adding keys to tables. One of the most common usecases of this is applying property changes without needing a Setter.
local propertyChangedSignal = Instance.new("BindableEvent")
local real = {a = 1}
local proxy = setmetatable({}, {
__index = a,
__newindex = function(self, k, v)
--self is the table itself, k is the key and v is the value
propertyChangedSignal:Fire(k) --fire a signal that the property was changed
real[k] = v --change the property in the real table.
--newindex prevents Lua from adding a key to the table itself meaning you can implement a proxy table like this
end
})
propertyChangedSignal.Event:Connect(print)
proxy.a = 5 --> (output from print connection: a, fired by setting the index)
proxy.a = 10 --> (output from print connection: a, fired AGAIN by setting the index)
Again, __newindex
can be set to a table, where it will just set the key in that table than the table itself. Similar to __index
.
The other metamethods
There’s a magnitude of metamethods to use, however most of them are pretty basic and self-explanatory, so I’ll only explain two other metamethods here.
You can see the others here
First of all, __tostring
. This metamethod is fired when you call tostring() on the table. Most commonly this is used in OOP to return the name of the object.
local o = setmetatable({}, {__tostring = function() return o.Name end})
o.Name = "Foo"
print(tostring(o)) --> Foo
And the second one, __metatable
. This can be used to ‘lock’ the table, however saying it locks the metatable is a bit misleading, since you can actually make it return anything that isn’t a function, and even that’s a bit misleading.
Basically, this metamethod makes getmetatable(table) return whatever __metatable
is pointing to, instead of the metatable itself
local table1 = setmetatable({}, {__metatable = "This metatable is locked"})
print(getmetatable(table1)) --> This metatable is locked
There’s another metamethod, __len
, which fires when the #
operator is used on the table, however due to some Lua 5.1 weirdness, this only works on userdatas.
Bypassing metamethods
Sometimes, you want to bypass metamethods, for example, setting the index of a table without firing __newindex
. Luckily, you have three new friends: rawset
, rawget
, and rawequal
. It is actually possible to stop these working, but that’s a topic for another day.
These methods work as following
-
rawget
- Get the raw value of a key without invoking the__index
metamethod -
rawset
- Set the raw value of a key without invoking the__newindex
metamethod -
rawequal
- Allows you to compare the raw value of two keys without invoking the__eq
metamethod
These methods dont work on userdatas, but again, that’s a topic for another day.
Summary
anyway, there’s metatables, told by someone who is bored