【问题标题】:lua OOP - how to write Class as a table of tables of functionslua OOP - 如何将类编写为函数表
【发布时间】:2018-04-21 21:57:21
【问题描述】:

上下文

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

我也将此作为一个学习机会。我的 OOP 技能很弱,而且我还没有完全理解元表。所以,如果我完全错过了一些东西,那么先发制人地抱歉。我正在按照Lua 编程第 3 版中基本 OOP 示例的样式编写所有代码。

我的问题

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

在代码中,我有一个类SomeClass。它有两种类型的打印功能:print1_notes 在'主表'中(即SomeClass 的键)。 print2.notes 在“嵌套”表print2 中,这是SomeClass 的键(即notes 只是SomeClass.print2 的键)。

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

  1. 如何在没有冒号运算符的情况下工作?
  2. SomeClass.print2 中的方法可以访问SomeClass 中的键吗?它们是否具有与非嵌套表键相同的访问权限?
  3. 我在想print2 需要一个__index 键来告诉它的方法在不知道self.a_key 是什么时查找SomeClass 中的键。但是self(在print2 方法中)不是关键。它实际上只是SomeClass 的别名。是吗?似乎与嵌套表存在差异。
  4. 是否需要将 SomeClass 设为 print2 的元表?这是否可能,因为 print2 不是 SomeClass 的单独表?
  5. 我应该尝试不同的方法吗?也许多重继承?

谢谢。抱歉,如果这需要移动,或者之前有人问过这个问题。

我的代码

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 避免了这种复杂性。 (我会在回答时解释更多)

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

【问题讨论】:

  • obj.print2:notes()obj.print2.notes(obj.print2) 的糖,而不是 obj.print2.notes(obj) 的糖
  • 是的。我没有意识到这一点;我猜该手册不够详尽。谢谢。

标签: oop lua


【解决方案1】:

从您的示例中,我猜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() 可能会尝试访问class 的一些notes 成员,这可能是一个表,但由于冒号语法,而是尝试访问@ 中的notes() 987654336@,这是一个函数,在尝试对其进行索引时会导致错误。

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

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

【讨论】:

  • 有关冒号语法如何工作的更多信息,我建议阅读此stackoverflow.com/questions/4911186/…
  • “你不能做class.print2:notes(),因为这相当于调用class.print2.notes(class.print2)”。这正是我所缺少的。手册没有描述嵌套表的冒号语法,所以现在这一切都说得通了。
  • " 至于你得到的错误,class.print2.notes() 可能会尝试访问类的一些注释成员,这可能是一个表,但是由于冒号语法,而是尝试访问 class.print2 中的 notes(),这是一个函数,在尝试对其编制索引时会导致错误。” 是的。将 print2.notes 更改为 print2.thenotes。对self.notes 的任何调用显然都替换为nil
  • “至于实际的解决方案,我想说你应该首先重新考虑你的代码结构。......你的类很臃肿并且做得比它应该做的更多,或者它不应该是一个自己的类,而是一个库、几个类,甚至可能是一个简单的函数。” 是的,这绝对是真的。我说我打算写这个大班系统,但我真的应该说我只是处于早期阶段。我现在只有这一节课,以后有很多新的组织计划空间。更何况,我还需要在Lua中学习多少,哈哈
  • 现在意识到obj = SomeClass:new() 实际上是obj = SomeClass.new(SomeClass)。我一直以为是obj = SomeClass.new(obj)。 *facepalm* 这是我在不理解代码的情况下复制和粘贴代码所得到的。
【解决方案2】:

在考虑了一段时间后,我注意到还有另一种方法可以解决它并保留命名空间的想法。

同样,您不能使用冒号语法将类传递给不在类中而是在类的 命名空间 中的函数。但是,您可以执行以下操作:

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() 时,它最终会在instance.notes 上调用print,但该函数仅在instance.notes.instance 上调用real 函数,它指向instance

【讨论】:

    【解决方案3】:

    我想出了一个解决方案。简而言之:print2 中的任何函数都必须以self = getmetatable(self) 开头。这是把self(原obj.print2)变成obj

    另外,如果此答案太长或违反任何准则,我们深表歉意。

    重申一下,我想要另一个表SomeClass.print2,它具有所有与打印相关的功能。如果我想打印笔记,我会这样做

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

    当然,当我这样做时,SomeClass.print2.notes 中的self.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() 内部,self 就是mt.print2。那么,self.key 应该变成:

    • obj.key 如果 keynotesN
    • 如果obj.key 为nil,则改为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
      • 所以我们在obj.print2的元表中寻找__index函数
      • mt.__index 返回SomeClass.print2.somekey
      • 所以obj.print2.notes(obj.print2) 现在是SomeClass.print2.notes(obj.print2)
    4. 通话内部(第 1 部分)
      • selfobj.print2
      • 我们想使用self.notes 得到obj.notes
      • 所以我们将self 更改为它的元表一次。我们做self = getmetatable(self)
      • self 现在是 mt.print2
      • 现在,如果我们使用self.notes,那么这将是mt.print2.notes
        • mt.print2.notes 为零
        • 这将调用obj.__index,因为objmt.print2 的元表
        • 所以mt.print2.notes应该变成obj.notes
        • 因此,self.notes 变为 obj.notes
    5. 通话内部(第 2 部分)
      • selfmt.print2
      • 我们可以再次self = getmetatable(self)
      • 然后self 将变成obj,因为这是mt.print2 的元表

    注意事项

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

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

    我创建mtmt.print2 的表的原因是我可以将更多这些“命名空间”函数添加到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 以与新对象关联。

    如果您能想到此技巧的任何性能问题/增强功能,请发表评论。或者,如果我能让它看起来更优雅。如果您能想到任何方法来删除每个 print2 函数定义顶部的 self = getmetatable(self),请务必告诉我。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-08-01
      • 2014-05-10
      • 2012-11-11
      • 2022-01-07
      • 1970-01-01
      • 1970-01-01
      • 2014-01-01
      相关资源
      最近更新 更多