类作为构造函数闭包

lua-users home
wiki

介绍

在十多年 Lua 编程生涯中,我尝试过许多面向对象 (OO) 的方法,包括“Programming in Lua”、“Lua Programming Gems”和这个 wiki 中的方法,以及我自己的一些方案,其中一些方案有大量的“C”支持库。最终,我将标准化到本文中概述的方案,该方案非常灵活且相对简单,可以在 Lua 中完全实现,并且具有一些我在其他 Lua 实现中没有见过的理想 OO 特性。

理想的 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”互操作

有关这些想法在“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 日)


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2014 年 2 月 24 日下午 12:03 GMT (差异)