类作为构造函数闭包 |
|
在十多年 Lua 编程生涯中,我尝试过许多面向对象 (OO) 的方法,包括“Programming in Lua”、“Lua Programming Gems”和这个 wiki 中的方法,以及我自己的一些方案,其中一些方案有大量的“C”支持库。最终,我将标准化到本文中概述的方案,该方案非常灵活且相对简单,可以在 Lua 中完全实现,并且具有一些我在其他 Lua 实现中没有见过的理想 OO 特性。
人们普遍认为,OO 系统应该提供对多个值(字段)和对这些字段进行操作的函数(方法)的封装。人们也普遍认为,该系统应该提供一种简单的方法来生成共享特征的多个对象(最常见的是方法实现)。类或原型是实现此目的的两种方法。
我还希望我的类能够无缝扩展 Lua 类型系统。在纯粹的 OO 语言中,类型和类之间没有区别,但对于 Lua 来说,一个好的折衷方案是类是类型,但反之则不然。给定一个未知的 Lua 值,我们需要一个函数来确定它是否属于指定的类或类型。
另一个理想的特性是在使用 Lua API 在“C”中创建的类和对象以及在纯 Lua 中创建的对象之间进行交互。这允许“C”库提供类,以及在这些库中定义的用于创建和返回对象的函数或方法。理想情况下,还应该允许“C”代码从 Lua 中定义的类创建对象。
本文提出的方案的核心是,类被实现为 Lua 函数闭包,这些闭包在执行时返回一个新的类对象。创建对象所需的所有资源都存储在类闭包的上值中,或者作为参数传递给闭包函数。这意味着类是独立于其存储的 Lua 一级值。
为了支持类型匹配,所有类中都必须存在一些特征化的上值。这被称为 TID,即类型 ID。还必须能够从类生成的物体中推导出 TID 值,以便通用谓词函数可以将物体与其类匹配。通过简单地将非类或物体值的 TID 定义为 Lua 类型名称,这种方案可以扩展到所有 Lua 值都具有 TID,并且可以使用相同的谓词函数进行类型匹配。
这种方案的一个很好的结果是,用于表示集合的物体的构造函数语法。例如,假设定义了一个名为 List 的类,我们可以这样写
mylist = List{"ham", "eggs", "toast"}
这使用了一个标准的 Lua 语法快捷方式,允许省略函数调用括号。在 List 函数内部,我们可以检测到传递了一个参数,一个没有元表的表(称为“原始表”),在这种情况下,我们可以将其就地转换为一个物体,而无需复制条目。我认为这似乎是对 Lua 表构造函数语法的非常自然的扩展。也可以使用其他参数签名提供替代的构造语法,或者作为替代方案。
在定义类实现方面相当规范之后,我们可以在保持通用类型匹配的同时,在物体实现方面更加灵活。在本节中,我将展示文献中建议的主要物体模式都可以包含在这个方案中。
首先,我将演示一个生成基于元表的物体的类。在这种模式中,物体是一个 Lua 表(或者如果在“C”中实现,则可能是一个用户数据),它具有与同一类别的所有物体共有的元表。类的函数和元函数存储在这个元表中,而字段存储在物体表中。元表也是类 TID。
do -- Class 'MetaBaseClass' local _TID = class.newmeta() -- Class Closure: local _C = function(p1) local o if class.istype(p1, 'rawtable') then o = p1 else o = {} end o.value = o.value or 0 setmetatable(o, _TID) return o end -- Methods: function _TID:mymethod() class.checkmethod(self, _C) print("Executing 'mymethod'") end -- Metamethods: function _TID.__add(p1, p2) class.checkmethod(p1, _C) class.checkmethod(p2, _C) local rv = _C() rv.value = p1.value + p2.value print("Executing add metamethod") return rv end -- Class Closure is a first-class value, for example, store it as a global: MetaBaseClass = _C end -- Class 'MetaBaseClass'
函数 class.newmeta()
是一个简单的辅助函数,它创建一个新的元表并将索引元函数设置为自引用。class.istype()
是一个多用途的类型测试谓词,class.checkmethod()
包装了这个测试,如果谓词为假则会引发错误(我将在本文后面展示这些函数的实现)。请注意,class.checkmethod
的常规使用也意味着类的所有函数都持有对类本身的上值引用,这有利于创建相同类的返回值参数。
要创建物体,只需调用类
obj1 = MetaBaseClass{value = 13}
obj2 = MetaBaseClass{value = 10}
obj1:mymethod()
obj3 = obj1 + obj2
print(obj3.value, 23)
为了支持单继承,class.newmeta()
可以扩展为接受一个父类。这将获取父类 TID(这也是其元表),并将其设置为新元表的元表(元元表?)。父类也作为语法便利性不变地传递。
do -- Class 'MetaChildClass' (Inherits MetaBaseClass) local _TID, _PC = class.newmeta(MetaBaseClass) -- Class Closure: local _C = function(p1) local o = _PC(p1) -- Extra initialisation for child class setmetatable(o, _TID) return o end -- Methods are inherited automatically, but may be overridden. -- To inherit metamethods add explicit delegations: _TID.__add = class.gettid(_PC).__add MetaChildClass = _C end -- Class 'MetaChildClass' obj4 = MetaChildClass{value = 2} obj4:mymethod() print((obj4 + obj1).value, 15)
我将探讨的第二种模式是基于原型的。传统上,原型方法根本不使用“类”的概念;相反,任何对象都可以充当创建更多对象的原型。但是,我将在原型对象周围使用标准的“包装器”类,并且它将实现表克隆代码。生成的物体是 Lua 表,其中包含方法和字段。如果需要元方法,还需要元表。为了支持类型匹配,即使元表为空,也会始终分配元表。
do -- Class 'ProtoBaseClass' local _C, _PRT, _MTB = class.newproto() _PRT.field = "Hello from ProtoBaseClass" function _PRT:method() class.checkmethod(self, _C) print(self.field) end ProtoBaseClass = _C end -- Class 'ProtoBaseClass'
支持函数 class.newproto()
在这种情况下完成了大部分工作。它生成类(函数闭包),并返回对原型表和元表的引用,这些引用已准备好填充。类函数是标准化的,提供了克隆原型表的功能。
obj5 = ProtoBaseClass() obj5:method() -- Prints 'Hello from ProtoBaseClass' obj6 = ProtoBaseClass{field="Hello from obj6"} obj6:method() -- Prints 'Hello from obj6'
标准类函数接受一个可选的表参数,该参数可以在从原型复制字段后覆盖(初始化)现有字段。
这种模式可以适应多重继承(聚合)。函数 class.newproto()
可以接受任意数量的对象(表)参数。这些表及其元表被合并以创建原型表和元表。合并按参数顺序进行,因此后面参数中的字段会覆盖前面参数中具有相同名称的字段。
do -- Class 'ProtoAggregateClass' local _C, _PRT = class.newproto(ProtoBaseClass()) _PRT.field = "Hello from ProtoAggregateClass" ProtoAggregateClass = _C end -- Class 'ProtoAggregateClass' obj7 = ProtoAggregateClass() obj7:method() -- Prints 'Hello from ProtoAggregateClass'
我将考虑的最后一种模式是单函数模式。它具有与类实现类似的对象实现(实际上,类也是这种类型的对象)。该对象只有一个方法,该方法与存储为上值的字段形成闭包。这种模式不支持继承或元方法。
do -- Class 'FunctionClass' local _TID = "FunctionClass" local _TID_O = _TID local _C = function() local tid = _TID local val = 0 return function() local tid = _TID_O val = val + 1 return val end end FunctionClass = _C end -- Class 'FunctionClass' obj8 = FunctionClass() obj9 = FunctionClass() print(obj8(), 1) print(obj8(), 2) print(obj9(), 1)
在这种模式下,TID 仅用于类型测试,并设置为一个任意但唯一的字符串。或者,可以使用空表。由于类和对象的实现无法通过 Lua 类型区分,因此使用扩展的标签约定。标签对于类来说恰好是“_TID”,但在对象中使用时带有后缀。根据此约定,类是对象,但对象不是类。
可以使用以下规则从任何 Lua 值派生 TID,这些规则按顺序应用,直到确定 TID 值为止
1. TID 是任何具有键“__tid”的元方法的非空值。
2. 如果该值为具有元表的表或用户数据,则 TID 为元表。
3. 如果该值为具有名称以“_TID”(标签)开头的上值的函数,则该上值为 TID。这适用于在 Lua 中创建的函数闭包。
4. 如果该值是一个函数,并且至少有两个未命名的上值,其中第一个上值的字符串值以“_TID”(标签)开头,那么第二个上值就是 TID。这适用于在 'C' 中创建的函数闭包。
5. 否则,该值的 Lua 类型就是 TID(它将是一个字符串)。
类是一个值,其 TID 由规则 3 或 4 确定,标签恰好是“_TID”。如果标签在“_TID”之后还有其他字符,则该值是一个对象,但不是一个类。
以下是使用纯 Lua 实现的 gettid
函数
do local getupvalue = require('debug').getupvalue class = class or {} class.gettid = function(v) -- Rule 1: metafield local mt = getmetatable(v) if mt and mt.__tid then return mt.__tid, "object_mf" end -- Rule 2: metatable if mt and (type(v) == 'userdata' or type(v) == 'table') then return mt, "object_mt" elseif type(v) == 'function' then local un, uv = getupvalue(v, 1) local r = "class" if un and un ~= "" then -- Rule 3: named upvalue local i = 2 while un and un:sub(1,4) ~= "_TID" do un, uv = getupvalue(v, i) i = i + 1 end if un and #un ~= 4 then r = "object_fn" end elseif un and type(uv) == 'string' and uv:sub(1,4) == "_TID" then -- Rule 4: upvalue pair with tag if #uv ~= 4 then r = "object_fn" end un, uv = getupvalue(v, 2) end if un then return uv, r end end -- Rule 5: Lua type name return type(v), "type" end end
获得类型(或类)的 TID 和值的 TID(或对象)后,身份由以下规则确定
1. 如果 TID 值是 Lua 等于函数,则调用该函数,并将值和类型传递给它。测试结果是第一个返回值的布尔值。
2. 如果值的 TID 是一个表,并且该表有一个键为 1 的条目,则如果类型的 TID 与从 1 开始的连续整数键值的任何条目匹配,则测试为真。
3. 如果值的 TID 是一个元表,而类型的 TID 是一个表,则如果类型 TID 与元表、元表的元表等等匹配,直到元表没有元表,则测试为真。
4. 如果以上情况都不适用,则测试是对两个 TID 值进行简单的 Lua 等于测试。
以下 Lua 函数实现了这些匹配规则,还结合了使用字符串代替类型参数的标准 Lua 类型检测、一些额外的有用类型谓词以及确定两个任意 Lua 值(除字符串外)是否为相同类型的能力
class.istype = function(vl, ty) local tvl = type(vl) local tid1, isc = class.gettid(vl) if type(ty) == 'string' and ty ~= "" then if ty == "class" then return isc == 'class' elseif ty == "object" then return isc ~= 'type' elseif ty == "rawtable" then if tvl ~= "table" then return false end return getmetatable(vl) == nil elseif ty == "callable" then if tvl == "function" then return true end local m = getmetatable(vl) if not m then return false end return type(m.__call) == "function" else return tvl == ty end end local tid2 = class.gettid(ty) if tid2 == tvl then return true end if type(tid1) == 'function' and tid1 == tid2 then return not not (tid1(vl, ty)) end if type(tid1) == 'table' and tid1[1] then for i=1, #tid1 do if tid2 == tid1[i] then return true end end end if isc == 'object_mt' and type(tid2) == 'table' then repeat if tid2 == tid1 then return true end tid1 = getmetatable(tid1) until not tid1 end return tid1 == tid2 end class.checkmethod = function(vl, ty) if not class.istype(vl, ty) then error("Bad method call") end end -- Export "istype" as a global since it will be widely used: istype = class.istype
元表模式的支持函数现在很简单
class.newmeta = function(pcl) local mt = {} if pcl then if not class.istype(pcl,'class') then error("Bad Parent Class") end local pmt = class.gettid(pcl) if not class.istype(pmt, 'table') then error("Bad Parent Class") end setmetatable(mt, pmt) else setmetatable(mt, nil) end rawset(mt, "__index", mt) return mt, pcl end
原型模式的支持函数更加微妙。外部函数将父对象合并到新的类原型表和元表中。此元表也是类的 TID。父对象的元表也在新的元表中以数字键的形式引用,这允许 `istype` 函数将此类的对象与任何父类或包含这些对象的类的对象进行匹配。类闭包中返回的内部函数将原型上值复制到新的对象表中,使用任何初始化表执行最终合并,并将 TID 上值设置为新对象的元表。
class.newproto = function(...) local _TID, _PRT, pmt = {}, {}, nil for i, t in ipairs{...} do if type(t) ~= 'table' then error("prototype must be table") end pmt = getmetatable(t) for k, v in pairs(t) do _PRT[k] = v end if pmt then for k, v in pairs(pmt) do _TID[k] = v end _TID[#_TID + 1] = pmt end end local _C = function(init) local tid, prt, ob = _TID, _PRT, {} for k, v in pairs(prt) do ob[k] = v end if init then for k, v in pairs(init) do if ob[k] == nil then error("attempt to initialise non-existant field: " .. k) end if type(k) ~= 'string' or k:sub(1,1) == '_' then error("attempt to initialise private field: " .. k) end ob[k] = v end end setmetatable(ob, tid) return ob end return _C, _PRT, _TID end
使用前面开发的示例类,以下类型测试都打印“true”。
print( istype(MetaBaseClass(), MetaBaseClass) ) print( istype(MetaChildClass(), MetaBaseClass) ) print( istype(MetaChildClass(), MetaChildClass) ) print( not istype(MetaBaseClass(), MetaChildClass) ) print( istype(MetaChildClass(), MetaBaseClass() ) ) print( not istype(MetaBaseClass(), MetaChildClass() ) ) print( istype(MetaBaseClass(), 'table') ) print( not istype(MetaBaseClass(), 'rawtable') ) print( istype({}, 'rawtable') ) print( istype(ProtoBaseClass(), ProtoBaseClass) ) print( not istype(ProtoAggregateClass(), MetaBaseClass) ) print( istype(ProtoAggregateClass(), ProtoAggregateClass) ) print( istype(ProtoAggregateClass(), ProtoBaseClass) ) print( istype(FunctionClass(), FunctionClass) ) print( istype(FunctionClass(), 'function') ) print( not istype(FunctionClass(), 'table') ) print( istype(FunctionClass(), 'callable') ) print( istype(FunctionClass(), 'object') ) print( not istype(FunctionClass(), 'class') ) print( istype(FunctionClass, 'class') ) print( istype(FunctionClass, 'object') ) print( istype(12, 'number') )
有关这些想法在“C”和 Lua 中部分混合实现的示例,请查看以下文件:`LibClass.h`;`LibClass.cpp` 和 `LibClass.lua`
https://github.com/JohnHind/Winsh.lua/tree/master/Winsh/Libraries
特别是,这些文件中 List 类的实现从“C”开始,随后在 Lua 中完成!
通过对“类”概念的实现进行规定,并通过实现高度灵活的类型匹配架构,我们可以对对象的实现更加宽松。同一 Lua 状态中的不同类可以使用多种不同的方式实现其对象,同时仍然保持一个同质的类型系统,其中任何值的类型都可以通过通用谓词函数进行匹配和表征。
将类实现为函数闭包,以便封闭的函数是该类对象的工厂函数或构造函数,带来了巨大的优势。值得注意的是,以这种方式定义的类可以是自包含的,并且是第一类 Lua 值,不依赖于注册表或全局资源,并且独立于存储。这种方法还利用了为命名函数参数提供的语法快捷方式,为集合类构造函数提供了一种语法,这似乎是 Lua 表构造函数的自然扩展。
JohnHind (2014 年 2 月 13 日)