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

в 19:13, , рубрики: 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: если для кого-то мой пост нечто очевидное — чтож, значит я себя переоценил :) Не судите строго, может кому-то зачем-то пригодится это решение!

Автор: Evengard

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js