给 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)