[LuaArray] 严格的 Lua 数组实现
说点什么
由于
Luatable的特殊构造,使用纯Lua实现 纯数组 是很困难的——table是Array和HashMap的混合体, 二者合二为一,你中有我,我中有你,水乳交融,难以切分。因此,Array很容易被污染而丧失Array的特性,HashMap亦然。
但table还有一个奇妙的特性——元表,借助 元表 的特殊运用,我们可以实现一个比较严格的数组Array。
基本要求
- 不可以存在非整数的索引;
- 可通过索引直接访问元素;
- 传入元素可直接创建一个数组,不传默认为空数组;
- 不允许数组索引越界;
- 提供插入、移除、修改数组元素等基本操作数组的方法 (有需要的话,可以按需求增加数组操作的方法,这里主要提供的是实现机制和思想);
- 数组索引自动管理。
硬性约束
- 不允许直接通过索引对元素设值为nil,也就是如下形式的赋值是不允许的:
arr[1] = nil;
这是因为对索引元素设置nil的话,数组的索引会被破坏; - 为了秉承
Lua的习惯,第一个元素的索引为1。
实现
还是 table
首先,我们创建一个数组构造函数:
function Array(...)
local __array__ = {...}
-- do something to construct an array
end
通过 Array() 来构造一个数组,其中 ... 是传入的元素参数,__array__ 用来存储数组元素。
嗯哼,目前看来,__array__ 可不还是一个table吗,其实还是什么都没有做呀。
别急,接下来,重头戏就来了。
变异的 table
为了构造一个严格的数组,我们首要的问题就是保证 __array__ 存储的都是整数的索引,这就要求我们就不能直接对 __array__ 进行操作,而要对它进行特殊保护——对外隐藏:
function Array(...)
local new_array = {}
local __array__ = {...}
local mt = {__index = new_array}
setmetatable(new_array, mt)
return new_array
end
为了实现隐藏的目的,我们增加了一个新的变量 new_array,让其元表指向 __array__,这样就可以直接通过索引访问数组元素,而又达到了隐藏真实数组的目的。
但这样还有两个严重的安全隐患:
- 通过
getmetatable(new_array).__index依然可以获得真实的数组数据; - 最终返回的构造数组将是
new_array,那么new_array必须具备数组的基本特性。
针对第一个问题,我们可以将__index 改为函数,实现彻底隐藏 __array__的目的:
function Array(...)
local new_array = {}
local __array__ = {...}
local mt = {
__index = function(t, k)
return __array__[k]
end
}
setmetatable(new_array, mt)
return new_array
end
现在我们来看第二个问题,new_array 将作为外交官,完成对真实数组 __array__ 的内部操作,那么 new_array 就必须看起来像真实的数组。为了保证 new_array 的纯净,我们必须再次改造它的元表。
function Array(...)
local new_array = {}
local __array__ = {...}
local mt = {
__index = function(t, k)
return __array__[k]
end,
__newindex = function(t, k, v)
if nil == __array__[k] then
print(string.format('warning : [%s] index out of range.', tostring(k)))
return
end
if nil == v then
print(string.format('warning : can not remove element by using `nil`.'))
return
end
__array__[k] = v
end
}
setmetatable(new_array, mt)
return new_array
end
我们对元表增加了 __newindex 的属性,它控制着对 new_array 的内部元素的附带副作用的操作。可以看出,目前我们只允许 new_array 修改 __array__ 已存在的元素的值 (且不允许对直接对元素设值为 nil,原因我们在 硬性约束中说过了),也就是所有与 __array__ 无关的操作都被过滤了。
到目前为止,new_array 已经是一个较为严格的 数组 了,下面就要对这个 数组 进行扩展了,毕竟,除了能够对已存在的元素进行赋值操作,现在的这个 数组 什么都做不了。
扩展 Array
为了扩展 Array 的操作属性,必须添加额外的方法对 __array__ 进行操作,我们像构造 __array__ 一样,创建一个变量 __methods__ 用来保存方法列表,同样,为了能够访问到 methods 中的方法,我们必须在元表中的 __index 添加 __methods__ 的访问途径 (在 web 中,这个概念是叫路由吧):
function Array(...)
local new_array = {}
local __array__ = {...}
local __methods__ = {}
function __methods__:insert(v, at)
local len = #__array__ + 1
at = type(at) == 'number' and at or len
at = math.min(at, len)
table.insert(__array__, at, v)
end
function __methods__:removeAt(at)
at = type(at) == 'number' and at or #__array__
table.remove(__array__, at)
end
function __methods__:print()
print('---> array content begin <---')
for i, v in ipairs(__array__) do
print(string.format('[%s] => ', i), v)
end
print('---> array content end <---')
end
-- extend methods here
local mt = {
__index = function(t, k)
if __array__[k] then
return __array__[k]
end
if __methods__[k] then
return __methods__[k]
end
end,
__newindex = function(t, k, v)
if nil == __array__[k] then
print(string.format('warning : [%s] index out of range.', tostring(k)))
return
end
if nil == v then
print(string.format('warning : can not remove element by using `nil`.'))
return
end
__array__[k] = v
end
}
setmetatable(new_array, mt)
return new_array
end
这里,为了方便展示,仅写了增、删两种方法,更多的还需要读者自己补充。我们来做一些 测试:
local arr = Array(1,2,3)
print(arr[1]) -- 1
print(arr[4]) -- nil
arr[1] = 4
arr:print() -- 4,3,2
arr[4] = 'a' -- warning : [4] index out of range.
arr[2] = nil -- warning : can not remove element by using `nil`.
arr:insert('a')
arr:insert('b', 2)
arr:print() -- 4,b,2,3,a
arr:removeAt(1)
arr:print() -- b,2,3,a
好了,现在一个较为严格的数组已经完成了,怎么用,就看大家的意愿了。
PS: 借助同样的机制,我们可以实现一个纯 HashMap,懒得动,就不写教程了吧。
参考:完整代码