When I create a class in Lua, there are always times when I need to use a getter or setter on attributes, instead of raw access. The way I’ve always done this is to use methods with names like getFoo
and setFoo
. And then to keep my API consistent, I have to switch every single property to use these getter/setter methods. The pain about these type of methods is that:
- You have to switch everything, even attributes that don’t need getter and setters, to keep your API consistent. This would end up slowing everything down.
- It doesn’t look as proper as using real getting and setting syntax, like
obj.foo
andobj.foo = v
. - It’s more to type.
What I really wanted was syntax like this:
obj.attr
obj.attr = something
I originally thought that implementing this would make things even slower, but that’s actually not the case. So let’s get started.
Implementation
Test = class('Test')
function Test:initialize()
self.foo = 3
end
function Test:getFoo()
return self.foo
end
function Test:setFoo(v)
self.foo = v
end
Here we have a small little MiddleClass class, which has a getter and setter method for the attribute foo
. Let’s see how we can improve its API.
Test = class('Test')
local mt = {}
mt.__index = function(self, key)
if self._props[key] ~= nil then
return self._props[key]
else
return Test.__classDict[key]
end
end
mt.__newindex = function(self, key, value)
if self._props[key] ~= nil then
self._props[key] = value
else
rawset(self, key, value)
end
end
function Test:initialize()
self._props = { foo = 3 }
local old = getmetatable(self)
old.__index = mt.__index
old.__newindex = mt.__newindex
end
I’ll admit, this code doesn’t look as nice (this could be wrapped into a mixin to make it look better), but the API results are awesome. What we’re doing here, it created a metatable with the __index
and __newindex
methods. For those who don’t know what these metamethods do, I suggest you go read about them in the Programming in Lua book.
Once we get inside of __index
we can trigger getter code. In this case I’m only looking in self._props
, but what you could do is check for a certain key name, and then run the proper getter code for it. But remember, you must also offer a way to get inside the __classDict
attribute of the class, as this is where all instance methods are stored.
EDIT: To support inheritance change ClassName.__classDict
to self.class.__classDict
. I found this out the hard way.
The same thing goes for __newindex
. We could check the key, and then trigger setting code; but in this case we’re just setting stuff in _props
. We also fall back to just setting something on the instance itself; if we didn’t do this, we would cripple the ability to set anything other than the pre-initialised properties (maybe this is what you want; the sky is the limit).
Right, after all that this allows us to do this:
local t = Test:new()
print(t.foo) -- 3
t.foo = 4
print(t.foo) -- 4
Speed Tests
Now let’s have a look at the speed. We’ll use this class for all the tests:
Test = class('Test')
local mt = {}
mt.__index = function(self, key)
if self._props[key] ~= nil then
return self._props[key]
else
return Test.__classDict[key]
end
end
mt.__newindex = function(self, key, value)
if self._props[key] ~= nil then
self._props[key] = value
else
rawset(self, key, value)
end
end
function Test:initialize()
self._props = { foo = 3 }
setmetatable(self, mt)
end
function Test:getFoo()
return self._props.foo
end
function Test:setFoo(v)
self._props.foo = v
end
local t = Test()
(If you’re wondering why I’m not using getmetatable
and then setting its properties, this is because this is the original code I was working with, and therefore run the tests. Soon after I noticed that using setmetatable
would destroy any other metatable before it, so I changed my code. However I couldn’t be bothered to re-run the tests – the results should be the same, but for test integrity I haven’t modified the code.)
The Original Way
We’ll test getting with this code:
for i = 1, 1000000 do
local a = t:getFoo() -- we'll need this assignment for the other tests later
end
It loops a million times, calling getFoo
. First my machine’s stats:
MacBook 2,1 - Mac OS X 10.6.6
2 Ghz Intel Core 2 Duo
2 GB DDR2 memory
Lua 5.1.4
And now the results of running time lua test.lua
:
real 0m0.577s
user 0m0.423s
sys 0m0.004s
Now for setting:
for i = 1, 1000000 do
t:setFoo(4)
end
Result:
real 0m0.535s
user 0m0.420s
sys 0m0.012s
The Awesome Way
For getting:
for i = 1, 1000000 do
-- this is where we need that assignment
-- lua won't accept just the code 't.foo'
local a = t.foo
end
Result:
real 0m0.295s
user 0m0.267s
sys 0m0.005s
For setting:
for i = 1, 1000000 do
t.foo = 4
end
Result:
real 0m0.323s
user 0m0.251s
sys 0m0.004s
Conclusion
So as you can see, not only does this method create a much nicer API (unless you love using get* and set* methods), but also improves speed quite a lot, which I didn’t expect. And take into consideration, that not only does this improve speed a lot on the front of getter and setter methods, but also allows you to leave to properties that don’t need getters and setters, just as plain properties; this will improve speed even more. So it’s a win both ways.
Enjoy!
EDIT: In the comments, Josh has noted that the reason why t:getFoo()
is slower is because it’s going through the same metatable as t.foo
, invoking a rawget
call. Without this, t:getFoo()
is actually a little faster.