Reputation: 15697
I'm writing a module to generate some kind of encrypted access tokens. The encryption library I'm going to use (lua-resty-string.aes
) uses the following method of class and class instance initialization:
local _M = { _VERSION = '0.14' }
local mt = { __index = _M }
function _M.new(...)
...
return setmetatable({ ... }, mt)
end
What is the fundamental difference of this initialization method with the following one (I'm more familiar with)?
Class = {}
Class.__index = Class
function Class.new()
return setmetatable({}, Class)
end
When I need to check is the variable passed to my function is an instance of the required class, for the second case I can use the following:
if getmetatable(obj) == Class then ...
However this is not working for the first case of initialization. I found the following works correctly:
local aes = require("resty.aes")
if (getmetatable(obj) or {}).__index == aes then ...
But I'm not sure is it OK or is there something more convenient to do it right? And what are the benefits of using the first method of class instance initialization?
Upvotes: 2
Views: 659
Reputation: 11201
First of all, _M
is just a naming convention for a module "namespace". You could name it "module" or "Class" as in your second example.The only real difference is the separation of metatable and "methodtable" / "class" table:
local Class = {}
local metatable = { __index = Class }
function Class.new(...)
...
return setmetatable({ ... }, metatable)
end
cleanly separates the two; the metatable is a local variable and thus kept "private" to the class (if the __metatable
field is set, it can further be hidden from getmetatable
). If the metatable should be exposed for convenient checks, one way to go would be a metatable
field in the Class
table:
Class.metatable = metatable
the other pattern just mushes the metatable and methodtable in the same table. Lua's metamethod naming using a double underscore as prefix (e.g. __index
) is designed to allow this:
local Class = {}
Class.__index = Class
function Class.new(...)
...
return setmetatable({ ... }, metatable)
end
it requires a circular reference in the Class
table (Class.__index
); the metatable can be accessed from anywhere as long as the Class
table is available because it is the metatable. Downsides of this include (1) potentially worse performance (due to hash collisions, because your metatable contains both the methods and the metamethods) and (2) somewhat dirty: If I inspected your Class
table, it won't be neatly separated in metamethods and class methods; I'd have to separate it myself based on method naming.
It also means that indexing instances or the Class
table allows "seeing" the underlying implementation which should probably be abstracted away / only accessible through indexing / using the operators / calling: Class.__index
and instance.__index
will be Class
.
Now to the problem of checking whether an object is an instance of a Class
: If the second approach is used, comparing the return value of getmetatable
works:
if getmetatable(obj) == Class then
-- object is an instance of the class
end
if the metatable is exposed as Class.metatable
or the like (may remind you of Object.prototype
in JS), this works very similarly:
if getmetatable(obj) == Class.metatable then
-- object is an instance of the class
end
alternatively, the Class
may implement it's own isinstance
method that checks against the "private" (local) metatable:
function Class.isinstance(table)
return getmetatable(table) == metatable
end
a third alternative to allow the simply comparison against Class
would be to set the __metatable
field to Class
:
local metatable = {__index = Class, __metatable = Class}
that way, the metatable
is neatly local
ized and separated from the Class
, yet from the outside it seems as if the "mushy" pattern was used.
If the metatable is however not exposed and no isinstance
method is available, you may have to resort to hacks such as the one you have shown to implement this:
if getmetatable(obj).__index == Class then
-- object is an instance of the class
end
this doees however rely on __index
being used in a specific way - a hidden implementation detail - and should, if possible, not be used in practice. Suppose __index
is changed to an actual metamethod to log a warning because a field has been deprecated:
function metatable.__index(self, field)
if field == "deprecated_field" then warn"deprecated_field is deprecated, use field instead!"
return Class[field]
end
and suddenly your check breaks because the assumption that metatable.__index == Class
simply doesn't hold anymore. An alternative hack to consider would be obtaining a metatable from an instance:
local instance = Class(...)
local class_metatable = getmetatable(instance)
...
if getmetatable(obj) == class_metatable then
-- object is probably an instance of the class
end
Upvotes: 2