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.fooandobj.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.