Классы в lua, или избавляемся от двоеточия

    Как всем известно, в lua нет как таковых классов и объектов. Однако есть метатаблицы и синтаксический сахар.
    С помощью указанных механизмов достаточно просто реализовать подобие классов.
    В итоге и получается нечто такое:
    Самый простой класс
    local MyClass = {} -- the table representing the class, which will double as the metatable for the instances
    MyClass.__index = MyClass -- failed table lookups on the instances should fallback to the class table, to get methods
    
    -- syntax equivalent to "MyClass.new = function..."
    function MyClass.new(init)
      local self = setmetatable({}, MyClass)
      self.value = init
      return self
    end
    
    function MyClass.set_value(self, newval)
      self.value = newval
    end
    
    function MyClass.get_value(self)
      return self.value
    end
    
    local i = MyClass.new(5)
    -- tbl:name(arg) is a shortcut for tbl.name(tbl, arg), except tbl is evaluated only once
    print(i:get_value()) --> 5
    i:set_value(6)
    print(i:get_value()) --> 6
    

    (взято с lua-users.org/wiki/ObjectOrientationTutorial)

    Всё это конечно хорошо, даже при определённой сноровке можно реализовать наследование…
    Но где public и private члены класса? Дефакто в этом примере они все public. Да ещё и надо помнить, где использовать двоеточие:
    MyClass:myFunc()
    

    а где просто одну точку:
    MyClass.myOtherFunc()
    

    А статические члены класса? Неужели придётся отказываться?


    Вот я и не захотел отказываться, и начал колхозить…

    Итак, представляю вам мой колхоз:
    Мой колхоз
    createClass = function()
    	local creator = {}
    	creator.__private = {
    		object_class = {},
    	}
    	
    	creator.__oncall = function(class_creator)		
    		-- Get the class definition so we can make needed variables private, static, etc.
    		local this = class_creator()
    		-- Initialize class from class definition
    		__init = function()
    			-- Init Public Static
    			local class = {}
    			if (type(this.__public_static) == "table") then
    				class = this.__public_static
    			end
    			
    			-- Init Object
    			local thisClass = this
    			local __constructor = function(...)
    				local object = {}
    				local this = class_creator()
    
    				-- Init Public
    				if (type(this.__public) == "table") then
    					object = this.__public
    				end
    
    				-- Init static values of the class
    				this.__public_static = thisClass.__public_static
    				this.__private_static = thisClass.__private_static
    
    				-- Call Constructor
    				if (type(this.__construct) == "function") then
    					this.__construct(...)
    				end
    				
    				-- Returning constructed object
    				return object
    			end
    			return {class = class, constructor = __constructor}
    		end
    
    		-- Creating class (returning constructor)
    		local class_data = __init()
    		local class = class_data.class
    		local constructor = class_data.constructor
    
    		-- Set class metatable (with setting constructor)
    		local class_metatable = {
    			__newindex = function(t, key, value)
    				if type(t[key])=="nil" or type(t[key])=="function" then
    					error("Attempt to redefine class")
    				end
    				rawset(t, key, value)
    			end,
    			__metatable = false,
    			__call = function(t, ...)
    				if type(t) == nil then
    					error("Class object create failed!")
    				end
    				local obj = constructor(...)
    				creator.__private.object_class[obj] = t
    				local object_metatable = {
    					__newindex = function(t, key, value)
    						class = creator.__private.object_class[t]
    						if type(class[key])~="nil" and type(class[key])~="function" then
    							rawset(class, key, value)
    							return
    						end
    						if type(t[key])~="nil" and type(t[key])~="function" then
    							rawset(t, key, value)
    							return
    						end
    						error("Attempt to redefine object")
    					end,
    					__index = t,
    					__metatable = false,
    				}
    				setmetatable(obj, object_metatable)
    				return obj
    			end,
    		}
    
    		-- Setting class metatable to the class itself
    		setmetatable(class, class_metatable)
    
    		-- Returning resulting class
    		return class
    	end
    	
    	return creator.__oncall
    end
    createClass = createClass()
    



    А пользоваться как? Очень просто, вот вам шаблон:
    myclass_prototype = function()
    	local this = {}
    	this.__public_static = {
    		-- Public Static Variables
    		statvalue = 5,
    		-- Public Static Funcs
    		statfunc = function()
    			print(this.__public_static.statvalue)
    		end,
    	}
    
    	this.__private_static = {
    		-- Private Static Variables
    		privstatdat = 2,
    		-- Private Static Funcs
    		privstatfunc = function()
    			print(this.__private_static.privstatdat)
    		end,
    	}
    
    	this.__public = {
    		-- Public Variables
    		pubdata = 3,
    		-- Public Funcs
    		pubfunc  = function(newprivate)
    			print(this.__public.pubdata)
    			this.__private.privdata = newprivate
    		end,
    	}
    
    	this.__private = {
    		-- Private Variables
    		privdata = 1,
    		-- Private Funcs
    		listallprivate = function()
    			print(this.__private.privdata)
    		end,
    	}
    
    	this.__construct = function()
    		-- Constructor
    	end
    
    	return this
    end
    
    myclass=createClass(myclass_prototype)
    

    Как видите, при каждом вызове изнутри класса придётся каждый раз указывать путь, а ля «this.__private.privdata», зато вот вам пример использования созданного класса!
    myobject = myclass()
    myobject.pubfunc(999)
    

    При вызове этого кода будет создан объект myobject из класса myclass, и будет вызвана функция pubfunc, которая высветит содержимое публичной переменной и изменит приватную.
    И никаких заморочек с двоеточиями!
    Кстати, статические вызовы тоже работают. Как из класса, так и из объекта.

    Итак, вкратце расскажу, что за магия здесь происходит. А происходит тут жонглирование так называемыми upvalues. upvalues — это переменные, которые изнутри видны, а снаружи — нет! Очень похоже на private, не так ли?
    Так вот, создав функцию-«прототип», мы создали новую область видимости, и в неё поместили все внутренности нашего класса, вынеся наружу только public и public static члены класса. А всю остальную магию выполняют метатаблицы, которые позволяют определить, что именно будет происходить при запросе «несуществующего» члена внешней таблицы, которая представляет наш класс/объект.
    Сумбурно звучит, знаю, но лучше не могу объяснить — не спец :)

    Долго думал, как можно сделать наследование при такой системе, но так и не придумал — upvalues достаточно серьёзно ограничивает наши действия, а извращенствами вроде debug библиотеки пользоваться не хотелось — она не везде включена. Если кто додумается, буду рад увидеть!

    PS: если для кого-то мой пост нечто очевидное — чтож, значит я себя переоценил :) Не судите строго, может кому-то зачем-то пригодится это решение!
    Support the author
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      +4
      > а где просто одну точку

      Везде можно писать двоеточие, так как двоеточие это «сахар»: Объект.функция(Объект, параметры) == Объект: функция(параметры). (upd: тут парсер всунул лишний пробел)

      Ваша реализация классов немного запутана: статик, паблик, приват и всё отдельными таблицами. У вас же есть основная таблица — вот вам и статик, вот вам и паблик. Хорошая идея, как мне кажется, хранить скрытые свойства в метаданных. Их никто не увидит и доступа к ним никто не получит, если сам «силой» не влезет через getmetatable(), но так же можно читать и userdata. Получается в духе Lua.

      Наследования можно осуществлять двумя путями:
      1. через какую-нибудь функцию extends('MyClass', родитель);
      2. через обращение к окружению: [local] MyClass = my_class_system(родитель);

      Второй метод интересен и хитёр, но нужно копаться с getfenv() и debug.getlocal(). И далеко не факт, что имена переменных не потеряются.
        0
        Тут в чём особенность. Дело всё в том, что у каждой таблицы (статик паблик, статик приват, просто паблик, просто приват) выставляется разная видимость. Потому и четыре разных таблицы — каждой аккуратно присваиваются свои права доступа. Если же доступ к основной таблице открывать, то тогда можно и до приватных добраться, чего я хотел избежать абсолютно.

        Двоеточие в принципе почему-то не понравилось, так то я прекрасно знаю, что можно всё через двоеточие писать (типичный сахар), но можно сказать, что челлендж был как раз от них избавиться) Мол, можно ли добиться того же без оного сахара?

        К getmetatable можно и закрыть доступ (__metatable = false). Думал как раз метатейблы использовать для наследования, но что-то не додумался как, не получилось. Да и дефакто метатейбл получается такой же скрытой Upvalue таблицей, её можно и не только при помощи метатейбла сделать. Впрочем, тут в принципе подход с upvalues достаточно жёстко ограничивает свободу действий.

        Насчёт getfenv() и debug.getlocal() — первый в lua 5.2 вообще вырезан, а вот debug бывает и отключён (debug = nil, особенно в каких нить embedded вариантах луа).

        Вообще, хотелось бы увидеть вашу реализацию, может и смог бы чего нибудь почерпнуть.
        0
        Точно, в Lua 5.2 getfenv заменили на _ENV.

        Я могу скинуть в личку ссылку на свой говнокод годичной давности, если интересно.

        P.S. Иногда, когда debug == nil явный require('debug') выручает ситуацию. Из-за того, что вырезают luaopen_debug() из автоматического подключения стандартных модулей.

        upd: прошу прощения, промахнулся.
          +1
          Зачем нужен весь этот колхоз не очень понятно:
          1. Есть готовые реализации, например, вот хорошая github.com/kikito/middleclass/
          2. Lua предполагает несколько другую парадигму вместо ООП, вот ей и надо пользоваться.

          А так получилась статья из серии «а смотрите как я могу». Можете, молодец. Но зачем это здесь? Кому нужен этот непонятный код?

          > Сумбурно звучит, знаю, но лучше не могу объяснить — не спец :)
          > Не судите строго, может кому-то зачем-то пригодится это решение!

          Ноу коммент. Филиал ЖЖ открыт?
            0
            Хочу лишь поблагодарить за ссылку и отметить правильность Вашего подхода (на тему парадигмы lua). Со всем остальным предпочту не согласиться.
            Целью моей попытки было проверить, возможно ли это, или нет, и насколько возможно. И я считаю, что своей статьёй кому-то помог, возможно даже навёл на мысль — мне большего и не нужно.

            PS: лично я считаю, что уж лучше такая статья, чем бесконечный поток малополезных новостей. Ну и смотрите хабы и теги.
              +1
              > И я считаю, что своей статьёй кому-то помог

              Как можно помочь плохой статьёй? Как можно помочь кодом, для понимания которого даже хорошему программисту надо затратить заметное время для понимания? Вы выложили код, почти без объяснения и думаете что это кому-то поможет? Тот кто может в нем сейчас разобраться — может наколхозить такого over 9000.

              И я даже не говорю ещё про то, что так в lua лучше не делать и про вред велосипедостроения.

              > что уж лучше такая статья, чем бесконечный поток малополезных новостей

              Это плохая позиция. «Остальные выкладывают трешак, ну и я выложу тоже почему нет».
                –1
                Я готов в меру своих возможностей ответить на вопросы по выложенному коду при необходимости. Пока вопросов нет — наверное, не всё так плохо, как Вам кажется.

                Лично я считаю, что lua настолько мощный язык, что в нём реально реализовывать самые разные парадигмы, в том числе классический ООП, что и пытаюсь показать этой статьёй. Опять таки, замечу, что неспроста разместил данную статью в блок «Ненормальное программирование» — ибо согласен, что всё это велосипедостроение и нарушение «парадигмы lua». Но… Ну и что! Возможно, не мне одному придётся как-нибудь реализовывать нечто подобное, вот и возможно этот код немного поможет реализовать нечто иное, но схожее.

                Дискуссия идёт в никуда, предлагаю её свернуть.
            0
            На практике все эти нагромождения искусственных классов мало нужны.
            Table вполне себе объект. И очень просто реализуются в нём private и public.
            Вот мой пример:
            local _M = {} -- module table
             
            _M.someProperty = 1 -- class properties
             
            local function createText()
               -- local functions are still valid, but not seen from outside - "private"
            end
             
            local privateVar -- so do local variables
             
            _GLOBAL_VAR -- without local it's global
             
            function _M.staticMethod(vars)
                -- this is class method like function (dot)
                -- there is no "self"
            end
             
            function _M:someMethod(vars)
                -- this is object method like function (colon)
                -- there is "self"
            end
             
            function _M:newBaseObject()
                -- Here goes constructor code
                local object = {}
                object.vars = 'some vars'
                object.name = 'BaseObject'
                object.property = self.someProperty -- from module
            
                local privateObjectVar = 'my_secret' -- private, can't be seen from outside
             
                function object.staticMethodInc(i) -- without colon it's "static"
                    return i + 1
                end
                
                function object:sign(song)
                    print(self.name .. ' is singing ' .. song)
                end
            
                function object:destroy()
                   -- optional destructor, after this to delete an object you just need to remove all references to it
                   self.vars = nil
                end
             
                return object
            end
             
            -- Now inheritance
            function _M:newChildObject()
                local object = self:newBaseObject()
                -- override any methods or add new
                object.name = 'ChildObject'
                function object:tell(story)
                    print(self.name .. ' is telling ' .. story)
                end
                return object
            end
             
            return _M -- return this table as a module to require()
            

            После загрузки этого модуля объекты создаются простым вызовом функций
            local MyNewObject = thatModule:newChildObject()
            
              0
              Моя цель была как раз избавиться от двоеточия :) Так-то всё абсолютно верно.
              Да и смущает меня local privateVar — это вы объявляете видимость в пределах файла. Если несколько классов в одном файле могут быть проблемы. Например для того же наследованного класса здесь вы не можете задать свои собственные статические переменные — они тот час же станут общими и для родительского, что считаю недопустимым. Впрочем кому как нравится.

              Да и ИМХО мой результирующий шаблон создания класса выглядит проще.

              На самом деле появилась идея и как с моим способом реализовать наследование, но это я уже буду экспериментировать, видимо, вне хабра.
                0
                >> Моя цель была как раз избавиться от двоеточия
                Ужас. Двоеточие как раз помогает.

                local privateVar — это именно private для модуля. Private для объекта это local privateObjectVar чуть ниже. Так что всё нормально с видимостью.

                Проще? Ну, хорошо, вот шаблон без всего лишнего:
                local function newObject()
                    local object = {}
                    object.publicProperty = 'Some property'
                    local privateVar = 'my_secret'
                 
                    function object:someMethod(value)
                        print(self.publicProperty, privateVar, value)
                    end
                
                    function object:destroy()
                       self.publicProperty = nil
                       privateVar = nil
                    end
                 
                    return object
                end
                

                Проще некуда.
                  0
                  Тут необходимо выносить весь класс в отдельный файл, чего так же не хотелось делать. local действует в текущем файле.
                  И таки да. Хоть двоеточие и «помогает», но плодит сущности. ИМО.
              0
              Спасибо, пригодилось!
                0
                Рад :) Если что-то доработали, то было бы интересно услышать что. :)

              Only users with full accounts can post comments. Log in, please.