给 Lua 增加参数类型描述
Lua 的函数定义是没有参数类型信息的。这些信息在跨语言的模块化设计中非常有价值。因为跨语言的方法调用通常需要做列集(Marshaling) 的操作,缺乏类型信息很难完成这个工作。同样的需求在做 RPC 调用的时候也很重要。
感谢 Lua 简洁的 metatable 的设计,我们可以用一个简单的方法自然的描述出 Lua 函数的调用参数类型,又无损性能。
一开始,我们先回顾一下在 Pil 中推荐的类实现方法。通常我们可以把方法列表(对应于 C++ 中的虚表)放在一个 table 中,然后把这个 table 作为一个 metatable 的 __index 方法,并把 metatable 附加到一个 table 上,就生成了简单的对象。我喜欢这样做:
foo={}
function foo:foobar(n)
print(self)
return n*n
end
foo={__index=foo}
function create(class,obj)
return setmetatable(obj or {}, class)
end
t = create(foo) -- 构造一个 foo 对象
t:foobar(100) -- 测试 foobar 方法
当然还有很多看起来更 cool 、更 OO 、更 C++ 的方法来在 Lua 下模拟一个类机制。云风以前也做过一个 ,其实后来还做过,或是见过身边认识的人做的“更漂亮”。只是请小心:Lua 不是 C++ 。实现一个超级 OO 的 Lua 代码风格跟解决问题是两件事,切莫迷失在这些语言技巧中。
有时候我喜欢这样定义成员函数:
local foo={}
setfenv(function ()
function foobar(self,n)
print(self)
return n*n
end
end,foo)()
for _,v in pairs(foo) do setfenv(v,_G) end
利用一个匿名函数,设置独立的 table 做为环境,有时可以方便许多。写起来更自由一些。这种利用独立环境的技巧有时也用来模拟类似 pascal 中的 with 。
下面是我们的正题:我们可以在此基础上给函数定义加上参数类型申明。
看起来函数定义的代码是这样的:
setfenv(function ()
def.foobar(table,number,
function (self,n)
print(self)
return n*n
end)
end,foo)()
这次的函数定义是用 def.function_name 达到的,并且把类型信息独立一行来写。第一行的类型名(table,number),第二行才是参数名(self,n)。
def table number string 这些可以在环境中定义好,作为这套体系中的保留字。它们可以为参数类型系统服务。def 可以是带元方法的空表,通过 __index 元方法捕获 . 后面的字符串参数。这样: 让 del.foobar 产生一个函数用来在环境中设置一个方法,并记录正确的类型信息(供别的框架设施使用)。如果你想在调试期做更强的类型检查,可以给注册的函数 function (self,n) 加一个壳,检查每个传入参数的类型是否匹配。甚至可以实现 Eiffel 那样的调用契约。
云风在下面给出一个简单但完整的实现。(比上面更多出记录了参数名字,语法是 def.foobar(table.self,number.n, 这些信息对于模块的方法自提供文档很有好处)
function test()
def.foobar(table.self,number.x,number.y,string.name,table.arg,
function(self,x,y,name,arg)
print(self)
return x+y
end)
end
local meta_arg_tostring={
__tostring=function (t) return t[1].." "..t[2] end
}
local function make_type(typename)
return setmetatable({typename},{
__tostring=function(t) return typename end,
__index=function(t,k) return setmetatable({typename,k},meta_arg_tostring) end,
})
end
local meta_type={__index=function(t,k) return k end }
local const_table = function (t,k,v) error "const table" end
local type_gen=setmetatable({
number=make_type "number",
boolean=make_type "boolean",
string=make_type "string",
table=make_type "table",
def=setmetatable({},{
__index = function (t,k)
return function(...)
local vtbl=getfenv(3)
vtbl.__meta[k]=arg
vtbl[k]=setfenv(arg[#arg],_G)
print(k,unpack(arg)) -- 输出函数的原型信息
end
end,
__newindex = const_table
})
},{__index=_G,__newindex = const_table } )
local class_vtbl = setmetatable ({},{__index=
function (t,k)
local ret={ __meta={} }
local gen=setfenv(k,type_gen)
setfenv(function () gen() end,ret)()
ret={ __index=ret }
t[k]=ret
return ret
end})
function create(class, obj)
return setmetatable(obj or {},class_vtbl[class])
end
local obj=create(test)
print(obj:foobar(1,2))
以上的代码可以直接在 lua 解释器中运行:将输出结果
foobar table self number x number y string name table arg function: 003E7C38 table: 003ED0D8 3
不多做解释了,弄明白原理的话,可以做更复杂的事情。比如模拟一个 enum,在函数定义里写 enum { "one","two","three" } 。或是增加返回值的描述,建议用语法:def(typename,typename,...).foobar(typename.arg1,typename.arg2)
甚至提供更强的,类似 COM 的接口描述:def.foobar(typename.in_out.arg1,typename.out.arg2)