【问题标题】:Structuring Lua classes构建 Lua 类
【发布时间】:2020-04-29 10:08:40
【问题描述】:

我正在 Lua 中构建一个类,其中包含许多相关函数组,但我不确定是否有更好的方法来构建它。我目前必须为 Lua 5.1 环境进行开发,但希望 Lua 5.3 在不久的将来成为可能。

该类将在许多不同的 Lua 程序中使用,所以我想要一些可以作为单个代码块放入的东西(我正在编程的环境意味着模块和 require 不是也不会) t 是一个选项)。

理想情况下,我想要一段黑盒代码(公开的公共方法除外),而不是在不同的类中重复代码(以提高可维护性)。

我目前拥有的是(概括的):

 function Fclass()

     --here I declare a bunch of local functions that can be called by any of the public methods

     local function A(parms)
     end

     --Public methods set 1

     --here I declare a bunch of state variables shared by BSelector and GetB

     local BSelector = function()
         A(parmvalues)
         --returns a bunch of iup controls with supporting (complicated) logic 
     end

     local GetB = function()
         --returns the values of the iup controls created in Bselector
     end

     --Public methods set 2

     --here I declare a bunch of state variables shared by DSelector and GetD

     local DSelector = function()
         --returns a bunch of iup controls with supporting (complicated) logic 
     end

     local GetD = function()
         A(parmvalues)
         --returns the value of the iup controls created in Dselector
     end

     return{BSelector =BSelector , GetB =GetB,  DSelector =DSelector , GetD =GetD}
 end

“B”和“D”组方法是完全独立的,除了它们都使用局部函数“A”等(不依赖于外部变量);理想情况下,它们的状态变量应该组本地

这是一个合理的结构吗?或者我应该将“B”和“D”组分成两个单独的类,然后复制本地函数或将它们作为单独的代码放入?我真的不想在类之外公开本地函数,因为不可避免地会出现命名冲突......大多数程序将使用所有方法组,尽管有些程序只使用一个组。

或者有更好的方法吗?

我这样调用它们:

myB = Fclass()
myD = Fclass()
someresults = myB.Bselector()
otherresults = myD.Dselector()

更新添加:我被告知我可能没有正确使用术语,我正在做的不是课程。我的方法基于Programming in Lua 并被选中是因为我想保留班级的状态变量?目的?私有的——只能通过公共方法访问。

【问题讨论】:

  • 它们是方法的函数吗?它们应该如何执行:f()x:f()
  • @EgorSkriptunoff 查看帖子末尾的编辑 -- 谢谢。
  • 答案将基于意见,但 FWIW,我认为您正在做的是定义类的好方法。
  • @ColeValleyGirl:“不要在不同的类中重复代码”我不明白这一点。您在 Lua 环境中工作,因此受到限制,您甚至无法使用 require 模块或(显然?)做任何类似的事情(例如 dofileloadstring 或类似的),这样您就可以构建自己的模块界面。那么您将如何避免代码重复?您必须为需要使用它的每个文件复制此类的源代码。
  • myB.Bselector() 好的:Bselector 究竟应该如何知道它被用于myB 而不是myD

标签: class lua scope


【解决方案1】:

您可以为您的类函数创建 2 个向上值。第一个值包含将由您的类的调用者访问的公共变量,例如函数本身和任何调用者处理的选项。

而第二个将用于您的私有值,这些值仅在班级内已知和可访问。您可以使用此私有表来存储不会暴露给调用者的内部状态或其他内部工作。

function class(first, second)

    local public = {first}
    local _private = {second}

    function _private.A(parms)
        --private function not available outside of class.
    end

    function public:selector() -- public class available to caller
        _private.A(parmvalues) -- calls private class
    end

    function public:get()
        return _private[1]
    end

    return public
end

myB = class('hello', ' world!') --passing in a variable for public, and one for private.
myD = class('hello...', ' world?')

print(myB[1] .. myB:get()) --get a public value, and uses get function to retrieve private value
print(myD[1] .. myD:get())

此外,如果您的用户不应该更改类函数,您可以通过将return public 更改为:

    local meta = {
        __index = public,
        __newindex = function(t, k, v)
                error("this table is read-only")
              end,
        __metatable = false
    }

    return setmetatable({}, meta) -- this make the public table read only

【讨论】:

    【解决方案2】:

    在您的示例中,您似乎通过闭包而不是表值封装了实例的状态。

    虽然这样做的优点是封装性更强,因为在不使用调试库的情况下,upvalue 从外部是不可见的,但它也带来了 Lua 必须关闭每个实例的每个方法,浪费更多内存(不是很多不过)。

    另一个好处是,当实例变量实现为表字段时,它们不需要在方法之前声明,因为表索引是基于字符串的,而当实现为闭包时,需要在函数之前知道局部变量已定义(这也适用于其他方法,它们在任一实现中的工作方式与实例变量相同)。

    更常见的做法是将实例变量存储为对象内的表值,并将对象作为第一个参数传递给函数。 There's even syntactic sugar for this.


    在 Lua 中创建类的方法有很多,需要权衡取舍(有些更擅长继承,而另一些则表现更好,等等)

    由于您似乎不需要任何继承,因此您可以使用简单的工厂函数,因为您已经在做很多事情了。

    我个人喜欢构建此类工厂函数的方式是:

    local object do
       local class = {}
       local meta = {__index=class} -- Object metatable
    
       function class:print() -- A method of the class
          print("Hello, I am an object and my x is " .. tostring(self.x))
       end
    
       function object(self) -- The factory function for the Class
          self.x = self.x or 0
          return setmetatable(self, meta)
       end
    end
    
    local o = object {
       x = 20
    }
    o:print()
    o.x = 30
    o:print()
    

    这样做的好处是,对于具有许多方法和许多实例的类,方法不会复制到每个实例中,这样可以节省一些内存。

    或者,你可以做这样的事情

    local object do
       local function object_print(self)
          print("Hello, I am an object and my x is " .. tostring(self.x))
       end
    
       function object(self)
          self.x = self.x or 0
          self.print = object_print -- Store method directly in the object
          return self
       end
    end
    

    同样,这会在每个实例中保存对每个方法的引用,浪费 一些 内存。好处是您现在可以将类视为特征。当你写

    person { name = "henry" }
    

    您可以将其视为创建一个名为 Henry 的新人,但您也可以将其视为创建一个名为 Henry 的对象并将人员特征添加到其中。

    由于将 OOP 的两个概念结合到一个实现中并且没有任何令人讨厌的继承,这是我在大多数简单情况下在 Lua 中构建对象的最喜欢的方式。


    更新

    特征方法也适用于一起定义多个类/特征:

    local person, human do
       -- Some generic method shared by both classes
       local function object_get_name(self)
          return self.name
       end
       -- Person uses this as a method, but human uses
       -- it as a function through an upvalue. Both work,
       -- but have different upsides and downsides.
    
       -- A method of person
       local function person_say_hi(self)
          print(self:get_name() .. " says hi!")
          -- Calling get_name as a method here
       end
    
       -- A method of human
       local function human_describe(self)
          print(object_get_name(self) .. ' is a human!')
          -- Calling get_name as an upvalue
       end
    
       function person(self)
          self.name = self.name or 'A person'
          self.say_hi = person_say_hi
          self.get_name = object_get_name
          -- Needs to be a method because person_say_hi assumes it to be one
          return self
       end
    
       function human(self)
          self.name = self.name or 'A human'
          self.describe = human_describe
          return self
       end
    end
    
    -- Create a new person
    local henry = person{ name = "Henry" }
    henry:say_hi()
    
    -- Create a new human
    local steve = human { name = "Steve" }
    steve:describe()
    
    -- Change the way henry gets his name
    function henry:get_name()
       return self.name:upper()
    end
    -- This only affects henry; all other "person" objects keep their old 
    henry:say_hi()
    -- This only works because say_hi accesses the method
    
    -- Add the person trait to steve
    person(steve)
    steve:describe() -- Steve is still a human
    steve:say_hi() -- Steve is also a person now
    

    【讨论】:

    • 我将不得不考虑这一点,因为我无法立即理解如何将它应用到我的场景中——尤其是在哪里为每个实例创建私有状态变量。如果我正在编写 VBA,我会立即知道如何执行此操作,并且在 Coral/Ada/Assembler 中这从来都不是问题......但我认为我找到了一个很好的“食谱”,除了我的问题概述,现在你让我质疑(不完全)一切。
    • 显然我错过了您的示例代码中的一些 cmets;我会再读一遍并更正我的答案
    • 这解释了我如何将状态变量实现为一个表——除非我是对的,否则我只能有一个表而不是在公共方法之间对变量进行分区?因此必须采用命名约定——Bx、Dx 而不是简单的 x(更易读,IMO)。大概我现在正在处理私有方法 - 或者这有什么缺点?会有几十个。
    • 我什至不确定我是否正确理解了这个问题;您有一组通用函数和两个都使用这些函数的类,并且想知道如何在不暴露函数的情况下分离这些类?是这样吗?方法呢?他们也分享方法吗?
    • 我有许多类/对象(最终将至少有 6 个)共享一组通用实用程序函数;但是每个对象都有不同的内部状态变量。每个对象会在一个或多个程序中被实例化 3-4 次,每个实例的状态变量需要是独立的。
    【解决方案3】:

    几年前,我为自己构建了一个在 Lua 中用于基本 OOP 功能的超类。

    用法:

    Person = LuaObject:extend({
        __name = "Person",
        name = "",
        age = 0,
    })
    
    -- constructor
    function Person:new(name, age)
        Person.__super.new(self)-- calling the super constructor
        self.name = name
        self.age = age
    end
    
    function Person:getName()
        return self.name
    end
    
    function Person:getAge()
        return self.age
    end
    

    请随意使用:

    --[[
    LuaObject for basic OOP in Lua
    Lua 5.0
    ]]
    local function newIndexFunction(tbl, name, value)
        if name == "new" and type(value) == "function" then
            local constructor = value
            rawset(tbl, name, function(self, ...)
                local object = self
                if object.__class == nil then
                    object = {}
                    object.__class = self
                    object.__id = string.sub(tostring(object), 8)
    
                    self.__index = self
                    setmetatable(object, self)
                end
    
                constructor(object, unpack(arg))-- Lua 5.0
                -- constructor(object, ...)-- Lua 5.1+
                return object
            end)
        else
            rawset(tbl, name, value)
        end
    end
    
    local function toStringFunction(tbl)
        return tbl:toString()
    end
    
    LuaObject = {__name = "LuaObject"}
    setmetatable(LuaObject, {__newindex = newIndexFunction, __tostring = toStringFunction})
    
    function LuaObject:extend(class)
        class = class or {}
    
        self.__index = self
        self.__newindex = newIndexFunction
        self.__tostring = toStringFunction
    
        local constructor = nil
        if class.new ~= nil then
            constructor = class.new
            class.new = nil
        end
    
        setmetatable(class, self)
    
        if constructor ~= nil then
            class.new = constructor
        end
    
        class.__super = self
    
        return class
    end
    
    function LuaObject:new()
    end
    
    function LuaObject:getSuperClass()
        return self.__super
    end
    
    function LuaObject:getClass()
        return self.__class
    end
    
    function LuaObject:toString()
        return string.format("[%s] %s", self.__class.__name, self.__id)
    end
    
    function LuaObject:isInstance(value)
        return value ~= nil and type(value) == "table" and getmetatable(value) == self
    end
    
    --[[
    -- examples
    -- basic class
    Person = LuaObject:extend({
        __name = "Person",
        name = "",
        age = 0,
    })
    
    -- constructor
    function Person:new(name, age)
        Person.__super.new(self)-- calling the super constructor
        self.name = name
        self.age = age
    end
    
    function Person:getName()
        return self.name
    end
    
    function Person:getAge()
        return self.age
    end
    
    -- extending classes
    Customer = Person:extend({
        __name = "Customer",
        id = 0,
    })
    
    function Customer:new(id, name, age)
        Customer.__super.new(self, name, age)
        self.id = id
    end
    
    function Customer:getID()
        return self.id
    end
    
    -- overriding methods
    function Customer:getName()
        -- calling super methods
        local realResult = Customer.__super.getName(self)
        if string.len(realResult) <= 5 then
            return realResult
        else
            return string.sub(realResult, 1, 5)
        end
    end
    
    -- testing
    local customer1 = Customer:new(1, "rollback", 19)
    local customer2 = Customer:new(2, "Kori", -1)
    
    print(assert(customer1:getName() == "rollb", "Overriding of getName failed"))
    print(assert(customer2:getName() == "Kori", "Overriding of getName failed"))
    print(assert(customer1:getID() == 1, "Error in getID"))
    print(assert(customer1:getAge() == 19, "Error in getAge"))
    print(customer1)
    ]]
    

    【讨论】:

      猜你喜欢
      • 2018-08-18
      • 1970-01-01
      • 2013-10-04
      • 2012-08-06
      • 2014-11-27
      • 1970-01-01
      • 2013-09-08
      • 2015-03-06
      • 2017-03-26
      相关资源
      最近更新 更多