lua OOP - 如何将Class编写为函数表的表

时间:2017-11-09 02:16:38

标签: oop lua

上下文

我试图写一个类系统,通过对音符和节奏进行排列来产生旋律。我将在工具" xStream"中使用它。在" Renoise"软件。我的例子是我真正拥有的一个非常愚蠢和通用的版本。

我也将此作为一个学习机会。我的OOP技能很弱,而且我仍然没有完全将脑袋缠绕在metatables上。如果我完全遗漏了某些东西,那就是先发制人的抱歉。我在第三版 Lua中的编程中使用基本OOP示例的样式完成了我的所有代码。

我的问题

我想要做的是将具有类似功能的类方法全部分组到'嵌套'表。例如,一个'旋律排列表,一个节奏排列表',一个杂项效用方法表等。

在代码中,我有一个班级SomeClass。它有两种类型的打印功能:print1_notes位于'主表中' (即SomeClass的关键字)。 print2.notes是一个嵌套的'表print2SomeClass的关键字(即notes只是SomeClass.print2的关键字。)

我可以打电话给print1_notes就好了。问题是当我用冒号运算符调用print2方法时。 如果我不使用糖(例如,obj.print2.notes(obj)),那么没问题。但是当我这样做时(例如obj.print2:notes()),我得到错误"尝试(等等)一个函数值..."。

  1. 如果没有冒号操作员怎么办?
  2. SomeClass.print2中的方法是否可以访问SomeClass中的密钥?他们的访问方式与非嵌套表键的访问方式相同吗?
  3. 当我不知道print2是什么时,我认为__index需要一个SomeClass密钥告诉其方法在self.a_key中查找密钥。但是self(在print2方法中)并不是一个关键。它实际上只是SomeClass的别名。是吗?似乎与嵌套表存在差异。
  4. 我是否需要将SomeClass设为print2的metatable?这是否可能,因为print2不是SomeClass的单​​独表格?
  5. 我应该尝试不同的方法吗?也许多重继承?
  6. 感谢。对不起,如果需要移动或者之前有人问过这个问题。

    我的代码

    SomeClass = {
        new = function (self, t)
            t = t or {}
            setmetatable(t, self)
            self.__index = function (_, key)
                return self[key]
            end
    
            --should I add a setmetatable here? perhaps:
            --setmetatable(self.print2, self)
    
            return t
        end,
    
    
        notes = {},
        set_notes = function (self, t)
            self.notes = t or {}
            self.N = #self.notes
        end,
    
        print1_notes = function (self)
            print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))
        end,
    
    
        --table of different print functions
        print2 = {
            notes = function (self)
                --is self an alias for SomeClass?
                assert(self.notes, "Error: self.notes = nil")
                print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))             
            end,    
    
            first_note = function (self)
                fn = self.notes[1]
                print("first note is: ", fn)
            end,
        },
    
    }
    
    
    
    obj = SomeClass:new()
    obj:set_notes{ 10,14,5, 10,14,5, 17 }
    
    
    print("\ncalling print1_notes without sugar:")
    obj.print1_notes(obj)
    print("\ncalling print1_notes with sugar:")
    obj:print1_notes()
    print("\ncalling print2.notes without sugar")
    obj.print2.notes(obj)
    print("\ncalling print2.notes with sugar")
    obj.print2:notes()  --this gives an error: "attempt to get length of a function value"
    
    
    obj.print2.first_note(obj)  --this works fine
    obj.print2:first_note()     --this gives an error: 
                                --  "attempt to index a function value (field 'notes')"
    

    编辑代码:tostring(N)的实例需要替换为tostring(self.N)

    编辑:奇怪的错误与SomeClass.print2.notes有像SomeClass.notes这样的笔记成员的事实有关。 SomeClass.print2.first_note避免了这种复杂情况。 (当我回答时,我会解释更多)

    编辑:我想出了一个解决方案。它不漂亮,但它有效。我将在下面发布我的答案。

3 个答案:

答案 0 :(得分:0)

从您的示例中,我猜Class也有一个字段notes,您尝试在print2.notes()中访问

这里的问题是lua并没有真正实现教科书对象的定位;如果你致电class:print_notes(...),你真的只需致电class.print_notes(class, ...)。如果您想致电class.print2.notes(class),则无法class.print2:notes(),因为这相当于致电class.print2.notes(class.print2)。你也不能写class:print2.notes(),因为它的语法无效;你只能使用:索引函数并在那里调用它们。

编辑:至于你得到的错误,class.print2.notes()可能会尝试访问notes的{​​{1}}成员,这可能是一个表,但由于冒号语法,而是尝试在class中访问notes(),这是一个函数,在尝试索引时会导致错误。

至于实际的解决方案,我说你应该首先重新考虑代码的结构。将函数组织到类中的命名空间是一种有点奇怪的方法,并且是一个强有力的指标,表明你的类是膨胀的并且做得比它应该更多,或者它不应该是它自己的一个类,而是一个库,几个类,或者甚至是一个简单的功能。

如果两种打印方法最终打印笔记,并且笔记是一个数组,那么为什么不用两种打印方法扩展该数组呢? luas OO的美妙之处在于对象和数据之间没有明确的界限,因此它取决于你看待它的方式。尽可能尝试使用这种力量,并且不要过分使用OO设计的教科书,这不是lua擅长或曾经用过的。

答案 1 :(得分:0)

在考虑了一段时间的问题之后,我注意到还有另一种方法来解决它并保持命名空间的想法。

同样,您不能使用冒号语法将类传递给不在类中但在类的名称空间中的函数。您可以的内容如下:

EventUser

正如您所看到的,每个实例都有自己的命名空间表,但它们都具有对同一共享函数的引用以及对它们所属实例的引用。当您致电local function print2(instance) -- Does things end local function wrapper(namespace) print2(namespace.instance) end function someClass.new() ... notes = {print=wrapper,instance=t} -- every instance needs its own namespace table ... end 时,它最终会在instance.notes:print()上拨打print,但该功能仅调用instance.notes上的真实功能,该功能指向instance.notes.instance

答案 2 :(得分:0)

我提出了各种各样的解决方案。简而言之:print2中的所有功能都必须以self = getmetatable(self)开头。这是将self(原obj.print2)转换为obj

此外,如果此答案太长或违反任何准则,请抱歉。

重申一下,我想要另一张表SomeClass.print2,其中包含所有与打印相关的功能。如果我想打印笔记,我会做

obj.print2:notes()
--sugar for
obj.print2.notes(obj.print2)

当然,当我这样做时,self.notes内的SomeClass.print2.notes会引用obj.print2.notes(而不是obj.notes)。即使我将函数重命名为SomeClass.print2.the_notes,这也是一个问题。

新代码

所以我所做的就是将SomeClass:new更改为:

SomeClass:new = function (self, t)
    t = t or {}                     -- 
    setmetatable(t, self)           --
    self.__index = function (_, k)  --
        return self[k]              --
    end                             -- same from before

    mt = {}
    mt.print2 = {}
    mt.print2.__index = SomeClass.print2  -- only fixes calls to obj.print2:foo()

    t.print2 = {}
    setmetatable( t.print2, mt.print2)
    setmetatable(mt.print2, t)
    t.__index = t  -- fixes references to self.key inside print2 functions

    return t
end

SomeClass.print2现在看起来应该是这样的:

SomeClass.print2 = {
    notes = function (self)
        self = getmetatable(self)   -- self is now mt.print2
                                    -- self.notes will become obj.notes
        self = getmetatable(self)   -- self should just be obj now
                                    -- but this is unnecessary

        print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))             
    end,    

    first_note = function (self)
        self = getmetatable(self)

        fn = self.notes[1]
        print("first note is: ", fn)
    end,
}

基本上,对obj.print2.foo的任何调用都应返回SomeClass.print2.foo。 (见mt.print2.__index)。

SomeClass.print2中的每个功能必须至少有一个' self = getmetatable(self)'在函数体的顶部**。然后,在SomeClass.print2.foo()内,selfmt.print2。那么,self.key应该成为:

    如果obj.keykeynotes ,则为
  • N
  • 如果obj.key为零,则变为SomeClass.key而不是

**建议使用第二个self = getmetatable(self),但这是可选的。

非常详细的解释

循序渐进,我们拥有的是

  1. 使用obj = SomeClass:new()创建新对象
    • objobj.print2
    • 处有一个空表
    • 创建了一个与mt
    • 相关联的新表obj
    • 更具体地说,mt.print2obj.print2的元表,而mt.print2的元表是obj
  2. 设置备注(例如obj:set_notes{10,20,30}
  3. 致电obj.print2:notes(),即obj.print2.notes(obj.print2)
    • obj.print2.somekey是nil,其中' somekey'是notes
    • 因此,我们会在__index
    • 的元表中查找obj.print2函数
    • mt.__index返回SomeClass.print2.somekey
    • 所以obj.print2.notes(obj.print2)现在是SomeClass.print2.notes(obj.print2)
  4. 通话内部(第1部分)
    • selfobj.print2
    • 我们希望使用obj.notes
    • 获取self.notes
    • 所以我们将self更改为其metatable一次。我们self = getmetatable(self)
    • self现在是mt.print2
    • 现在,如果我们self.notes,那将是mt.print2.notes
      • mt.print2.notes是nil
      • 这会调用obj.__index,因为objmt.print2
      • 的元表
      • 因此mt.print2.notes应该成为obj.notes
      • 因此,self.notes变为obj.notes
  5. 通话内部(第2部分)
    • selfmt.print2
    • 我们可以再次self = getmetatable(self)
    • 然后self将成为obj,因为它是mt.print2
    • 的元数据
  6. 注释

    这是一个非常难看的解决方案,我承认。但它的确有效。

    我讨厌将self = getmetatable(self)放在SomeClass.print2中任何函数体的顶部。如果在self内将obj转换为mt.print2.__index,则可以避免这种情况。我非常确定这是不可能的,因为mt.print2.__index只能返回一个对象(即函数SomeClass.print2.foo)。

    mt使用mt.print2的表格的原因是我可以添加更多这些' namespaced'函数SomeClass。如果我想要一个名称空间m_notes用于转换注释的函数,我只需将这些语句添加到SomeClass:new()

    mt.m_notes = {}
    mt.m_notes.__index = SomeClass.m_notes
    
    t.m_notes = {}
    setmetatable( t.m_notes, mt.m_notes )
    setmetatable(mt.m_notes, t)
    

    事实上,我可以创建一个为任何新命名空间执行此操作的函数:

    add_namespace = function( t, mt, key_string )
        mt[key_string] = {}
        mt[key_string].__index = SomeClass[key_string]
         t[key_string] = {}
        setmetatable( t[key_string], mt[key_string] )
        setmetatable(mt[key_string], t)
    end
    

    也许最好让mt成为t中的成员。因此mt只是t.mtmt.print2将是t.mt.print2等等。然后,如果需要,可以稍后访问它(例如,出于封装目的)。单独拥有mt并没有任何问题;调用SomeClass:new()将始终创建与新对象关联的新mt

    请注意,如果您能够通过此技巧考虑任何性能问题/增强功能。或者,如果我可以使它看起来更优雅。如果您能想出以任何方式删除每个self = getmetatable(self)函数定义顶部的print2,请务必告诉我。