在 Lua 中定义类型的简单方法
我通常用 Lua 定义一个类型只需要这样做:
-- 定义一个 object 的新类型
local object = {}; object.__index = object
-- 定义构建 object 的函数
local function new_object(self)
return setmetatable(self or {}, object)
end
-- 给 object 添加一个 get 方法
function object:get(what)
return self[what]
end
-- 测试一下
local obj = new_object { x = "x" }
assert(obj:get "x" == "x")
这样写足够简单,如果写熟了就不用额外再做封装。如果一定要做一点封装,可以这样:
local class = {}; setmetatable(class, class)
function class:__index(name)
local class_methods = {}; class_methods.__index = class_methods
local class_object = {}
local class_meta = {
__newindex = class_methods,
__index = class_methods,
__call = function(self, init)
return setmetatable(init or {}, class_methods)
end
}
class[name] = setmetatable(class_object, class_meta)
return class_object
end
封装的意义在于:你可以通过上面这个 class 模块定义新的类型,且能通过它用类型名找到所有定义的新类型。而上面的第一版通常用于放在独立模块文件中,依赖 lua 的模块机制找到 new_object 这个构建方法。
而封装后可以这样用:
-- 定义一个名为 object 的新类型,并添加 get 方法:
local object = class.object
function object:get(what)
return self[what]
end
-- 创建新的 object 实例,测试方法 object:get
local obj = class.object { x = "x" }
assert(obj:get "x" == "x")
如果觉得 local object = class.object 的写法容易产生歧义,也可以加一点小技巧(同时提供特殊的代码文本模式,方便日后搜索代码):
function class:__call(name)
return self[name]
end
-- 等价于 local object = class.object
local object = class "object"
如果我们要定义的类型是一个容器该怎么做好?
容器的数据结构有两个部分:容纳数据的集合和容器的元数据。之前,我通常把元数据直接放在对象实例中,把集合对象看作元数据中的一个。
比如定义一个集合类型 set 以及两个方法 get 和 set :
local set = class "set"
function set:new()
return self {
container = {},
n = 0,
}
end
function set:set(key, value)
local container = self.container
if value == nil then
if container[key] ~= nil then
container[key] = nil
self.n = self.n - 1
end
else
if container[key] == nil then
self.n = self.n + 1
end
container[key] = value
end
end
function set:get(key)
return self.container[key]
end
真正集合容器在 self.container 里,这里 self.n 是集合的元信息,即集合元素的个数。注意这里集合类型需要有一个构造函数 new ,因为它在构造实例时必须初始化 .n 和 .container 。这里的 set:new 构造函数调用了前面生成的 class.set 这个默认构造行为。
测试一下:注意这里用 class.set:new() 调用了构造函数。它等价于 class.set { container = {}, n = 0 } ,因为 .container 和 .n 属于实现细节,所以不推荐使用。
local obj = class.set:new()
obj:set("x", 1)
obj:set("y", 2)
assert(obj.n == 2)
assert(obj:get "x" == 1)
如果使用者要直接访问容器的内部数据结构,它可以用 obj.container 找到引用。但我们可能希望 set 表现得更像 lua table 一样,所以也可能想这样实现:
local set2 = class "set2"
function set2:new()
return self {
_n = 0,
}
end
function set2:set(key, value)
if value == nil then
if self[key] ~= nil then
self[key] = nil
self._n = self._n - 1
end
else
if self[key] == nil then
self._n = self._n + 1
end
self[key] = value
end
end
-- 测试一下
local obj = class.set2:new()
obj:set("x", 1)
obj:set("y", 2)
assert(obj._n == 2)
assert(obj.x == 1)
这个版本去掉了 .container 而直接把数据放在 self 里。所以不再需要 get 方法。为了让元数据 n 区分开,所以改为了 ._n 。
如果规范了命名规则,用下划线区分元数据未尝不是一个好的方法,但在迭代容器的时候会需要剔除它们比较麻烦。所以有时候我们会把元数据外置,这里就需要用到 lua 5.2 引入的 ephemeron table 来帮助 gc 。
local set3 = class "set3"
local SET = setmetatable({}, { __mode = "k" })
function set3:new()
local object = self()
SET[object] = { n = 0 }
return object
end
function set3:set(key, value)
if value == nil then
if self[key] ~= nil then
self[key] = nil
SET[self].n = SET[self].n - 1
end
else
if self[key] == nil then
SET[self].n = SET[self].n + 1
end
self[key] = value
end
end
function set3:__len()
return SET[self].n
end
-- 测试一下:
local obj = class.set3:new()
obj:set("x", 1)
obj:set("y", 2)
assert(#obj == 2)
assert(obj.x == 1)
-- 迭代 obj 已经看不到元数据了。
for k,v in pairs(obj) do
print(k,v)
end
由于 ._n 外部不可见,所以我们用 #obj 来获取它。
如果不想用 ephemeron table 管理元数据,是否有什么简单的方法剔除元数据呢?
最近发现另一个小技巧,那就是使用 false 作为元数据的 key :
local set4 = class "set4"
function set4:new()
return self {
[false] = 0,
}
end
function set4:set(key, value)
if value == nil then
if self[key] ~= nil then
self[key] = nil
self[false] = self[false] - 1
end
else
if self[key] == nil then
self[false] = self[false] + 1
end
self[key] = value
end
end
function set4:__len()
return self[false]
end
-- 测试一下
local obj = class.set4:new()
obj:set("x", 1)
obj:set("y", 2)
for k,v in pairs(obj) do
if k then
print(k,v)
end
end
这个版本几乎和第二版相同,不同的地方只是在于把 ["_n"] 换成了 [false] 。这里只有一个元数据,如果有多个,可以把 [false] = {} 设为一张表。
这样就不需要额外使用弱表,在迭代时也只需要判断 key 是否为真来剔除它。虽然有这么一点点局限,但贵在足够简单。
当然你也可以给它再定义一个 __pairs 方法滤掉 false :
function set4:next(k)
local nk, v = next(self, k)
if nk == false then
return next(self, false)
else
return nk, v
end
end
function set4:__pairs()
return self.next, self
end
或者给加一种叫 class.container 的类型创建方法
local function container_next(self, k)
local nk, v = next(self, k)
if nk == false then
return next(self, false)
else
return nk, v
end
end
function class.container(name)
local container_class = class[name]
function container_class:__pairs()
return container_next, self
end
return container_class
end
如果你不需要 class 提供的默认构造函数,同时不喜欢定义一个新的 new 方法,也可以直接覆盖默认构造函数(同时避免别处再给它增加新的方法):
local set5 = class.container "set5"
function set5:set(key, value)
if value == nil then
if self[key] ~= nil then
self[key] = nil
self[false] = self[false] - 1
end
else
if self[key] == nil then
self[false] = self[false] + 1
end
self[key] = value
end
end
function set5:__len()
return self[false]
end
function class.set5()
return set5 {
[false] = 0,
}
end
local obj = class.set5()
obj:set("x", 1)
obj:set("y", 2)
for k,v in pairs(obj) do
print(k,v)
end
Comments
Posted by: GL | (7) October 2, 2025 01:51 PM
Posted by: nujabes | (6) September 10, 2025 11:32 AM
Posted by: JulyWind | (5) August 28, 2025 02:52 AM
Posted by: Cloud | (4) August 27, 2025 07:27 PM
Posted by: hanxi | (3) August 27, 2025 08:52 AM
Posted by: Cloud | (2) August 26, 2025 12:12 PM
Posted by: 法外狂徒 | (1) August 26, 2025 11:58 AM