Версия для печати темы
MyDC.ru _ Программирование на Lua _ Объектно-Ориентированное Программирование в Lua
Автор: Setuper 13.12.2008, 15:01
Объектно-Ориентированное Программирование (ООП) в LUA
В данной теме обсуждаются мощнейшие методы ООП
Итак, потихоньку буду писать основные понятия.
Объект и класс
Прежде всего следует понять что такое объект, и что такое класс
Согласно языку С++, класс - это определяемый пользователем тип, а объект - это экземпляр класса.
Попробуем в lua определить понятие класса.
Каждому классу могут соответствовать несколько объектов. Каждый объект занимает своё место в памяти и манипулирует со своими переменными и функциями. Приведу пример класса и объектов:
Возьмём класс - автомобиль. У этого класса есть некие свои "переменные" и "функции", такие как цвет (функция определения цвета), тип (хэтчбек, универсал, седан) и тд.
Объектом данного класса, например, будет красный седан
Другим объектом будет синий универсал
Третьим объектом будет также синий универсал
Второй и третий объекты не отличаются ничем, однако, они разные объекты, физически разные машины, хоть и выглядят одинаково (они занимают разные адреса в памяти).
Итак, класс - это некая общность объектов.
Конструктор
Функция, которая предназначается для инициализации (создания) объектов называется конструктором. Ввиду того, что такая функция создаёт (конструирует) объект. В С++ имя конструктора совпадает с именем класса, и вызывается при выделении памяти под объект.
В lua бы будем использовать для конструктора имя: __init, а вызываться конструктор будет при создании объекта.
Пример:
Код
class.MyClass {
__init = function(a, b, c) -- конструктор с параметрами a, б и c
... -- описание конструктора
end
}
MyObject = MyClass(1, 2, 3) -- создание объекта (вызов конструктора)
То есть при создании объекта обязательно сначала вызывается конструктор. В приведённом примере при создании объекта по средствам конструктора в объект передаются некоторые значения: 1, 2, 3. Если конструктор не содержит передаваемых параметров, то такой конструктор называется конструктором по умолчанию.
ДеструкторДеструктор вызывается каждый раз при уничтожении объекта. Это полная противоположность конструктору.
Закрытые переменные (private переменные)Закрытые переменные - это переменные, которые могут использоваться только членами-классами.
В lua закрытые переменные у нас будут содержать 2 подчёркивания спереди.
Защищённые переменные (protected переменные)Защищённые переменные - это переменные, которые могут наследоваться
В lua защищённые переменные у нас будут содержать 1 подчёркивание спереди.
продолжение следует...
Автор: Setuper 27.2.2009, 0:32
ООП широко распространено из-за его эффективности. Эта эффективность выражается в экономном использовании памяти и ресурсов компьютера. Достигается это всё при помощи таких мощных аппаратов как инкапсуляция, наследование и полиморфизм и абстракция.
Реализация класса, которая будет описываться в данном топике является первым вариантом, и служит для введения в курс дела. В своих проектах же не следует её использовать. В проектах используйте доработанную версию библиотеки.
Итак, во-первых, не вдаваясь в подробности, напишу основную функцию, которая содержит в себе все выше изложенные аппараты.
Те, кто достаточно хорошо знают язык LUA, могут попытаться понять как это всё работает, ну а всем остальным нужно только скопировать без понимания - понимать нужно будет дальше (после этого кода).
Код
local _G = _G
local function concat(p1, p2)
local t = {}
for k, v in _G.pairs(p1) do
t[k] = v
end
for k, v in _G.pairs(p2) do
t[k] = v
end
if _G.next(p1.__parent or {}) or _G.next(p2.__parent or {}) then
t.__parent = concat(p1.__parent or {}, p2.__parent or {})
end
return t
end
cClass = function(...)
local t, m = {}, {}
_G.setmetatable(t, m)
for i = 1, _G.select('#', ...) do
local p = _G.select(i, ...)
if _G.type(p) == 'table' then
for k, v in _G.pairs(p) do
t[k] = v
end
end
end
m.__call = function(self, ...)
local t, mt_old, mt_new = {}, {}, {}
for k, v in _G.pairs(_G.getmetatable(self) or {}) do
mt_old[k] = v
end
for k, v in _G.pairs(self) do
mt_old[k] = v
end
for i = 1, _G.select('#', ...) do
local p = _G.select(i, ...)
if _G.type(p) == 'table' then
for k, v in _G.pairs(_G.getmetatable(p) or {}) do
mt_new[k] = v
end
for k, v in _G.pairs(p) do
t[k] = v
end
end
end
mt_new = concat(mt_old, mt_new)
t.__parent = mt_new
mt_new.__index = mt_new
_G.setmetatable(t, mt_new)
return t
end
return t
end
Приведённая функция (cClass) представляет из себя функцию для реализации класса.
Давайте же попробуем написать свой класс, используя эту самую функцию:
Код
cMyFirstClass = cClass{
mMember1 = 5;
mMember2 = "first_class_mMember2";
func1 = function(self)
Core.SendToAll(self.mMember2)
end
func2 = function(self)
Core.SendToAll"Example"
end
}
Функция содержит 2 члена (числовой и строковый) и два метода (func1, func2).
Выведем содержимое всех членов класса на экран:
Код
for k, v in pairs(cMyFirstClass) do
Core.SendToAll("["..k.."] = "..tostring(v))
end
На экране видим следующее:
Цитата
[mMember1] = 5
[func1] = function: 01A57710
[func2] = function: 01A525B0
[mMember2] = first_class_mMember2
Наследование.Теперь рассмотрим аппарат наследования.
Построим ещё один класс (будем его называть производным классом), который будет наследовать все члены и функции нашего (базового) класса cMyFirstClass.
Код
cMySecondClass = cMyFirstClass{
mMember2 = "second_class_mMember2";
func3 = function(self)
self:func2()
end
}
Если теперь мы выведем на экран все члены класса:
Код
for k, v in pairs(cMyFirstClass) do
Core.SendToAll("["..k.."] = "..tostring(v))
end
то мы увидим следующее:
Цитата
[mMember2] = second_class_mMember2
[func3] = function: 01A12170
[__parent] = table: 01A640F0
то есть, мы видим элемент mMember2 и метод func3, но также мы можем наблюдать некую таблицу __parent. Это таблица базового класса (cMyFirstClass).
Полиморфизм.К любым элементам базового класса можно обращаться через эту самую таблицу __parent. Например, я не могу из класса cMySecondClass обратиться непосредственно к элементу mMember2 класса cMyFirstClass, так как в классе cMySecondClass есть элемент с точно таким же именем и при обращении именно он будет возвращаться:
Код
Core.SendToAll(cMySecondClass.mMember2)
на экране видим
Цитата
second_class_mMember2
Однако, если написать так:
Код
Core.SendToAll(cMySecondClass.__parent.mMember2)
мы увидим на экране желаемый результат:
Цитата
first_class_mMember2
Инкапсуляция.Теперь рассмотрим аппарат инкапсуляции.
Код
local cMyFirstClass = {}
do
local mMember3 = "private_mMember3"
cMyFirstClass = cClass{
mMember1 = 5;
mMember2 = "first_class_mMember2";
func1 = function(self)
Core.SendToAll(self.mMember2)
end
func2 = function(self)
Core.SendToAll"Example"
end
}
end
Инкапсуляция предполагает закрытие доступа к переменной вне класса. Для реализации инкапсуляции используем блок do ... end. Несколько не обычный ход, не правда ли?
Проверим содержимое таблицы __parent класса cMySecondClass:
Код
for i,v in pairs(cMySecondClass.__parent) do
SendToAll("["..i.."] = "..tostring(v))
end
На экране видим следующее:
Цитата
[mMember2] = first_class_mMember2
[mMember1] = 5
[func1] = function: 01A78A60
[func2] = function: 01A62500
[__index] = table: 01A454B8
[__call] = function: 01A78AD0
На переменные __index и __call можем не обращать внимания (они являются метаметодами).
Теперь, для чего всё это нужно?А нужно это для значительной оптимизации работы и для структурированного написания скриптов.
Дело вот в чём. Создавая класс cMyFirstClass, под него выделяется определённое количество памяти. Память, выделенная под класс cMySecondClass, - это память выделенная под переменные mMember2 и func3, а подо все унаследованные переменные память не выделяется (она уже выделена под класс cMyFirstClass) (тут имеется ввиду выделяемая память под реализацию функций). Поэтому получаем большую функциональность при малом выделении памяти и при малом количестве строк реализации.
В конце вопрос на засыпку:
Что выведет на экран метод cMySecondClass:func1() ?
Ответ: second_class_mMember2
Автор: Setuper 17.5.2009, 23:02
Библиотека построения именованых классов от разработчика lua.
Самая продвинутая реализация классов и практически всех принципов ООП, включая виртуальные классы!
classlib.lua ( 11.8 килобайт )
: 92
Упрощённая библиотека построения безымянных классов от разработчика lua.
unclasslib.lua ( 10.84 килобайт )
: 51
В дальнейшем мною будут выложены подкорректированные варианты, так как в данных вариантах мною были найдены мелкие недочёты.
Автор: Setuper 20.5.2009, 14:05
Для того чтобы проследить работу с библиотекой классов и понять как всё это работает, предлагаю продебажить следующий простой пример. Дебажить можно с помощью средств LuaEdit.
Код
class.Account()
function Account:__init(initial)
self.balance = initial or 0
end
function Account:deposit(amount)
self.balance = self.balance + amount
end
function Account:withdraw(amount)
self.balance = self.balance - amount
end
function Account:getbalance()
return self.balance
end
-----------------------------------
class.NamedAccount(shared(Account)) -- shared Account base
function NamedAccount:__init(name, initial)
self.Account:__init(initial) -- 10.00
self.name = name or 'anonymous' -- 'John'
end
-----------------------------------
class.LimitedAccount(shared(Account)) -- shared Account base
function LimitedAccount:__init(limit, initial)
self.Account:__init(initial) -- 10.00
self.limit = limit or 0 -- 0.00
end
function LimitedAccount:withdraw(amount)
if self:getbalance() - amount < self.limit then
error 'Limit exceeded'
else
self.Account:withdraw(amount)
end
end
-----------------------------------
class.NamedLimitedAccount(shared(NamedAccount), shared(LimitedAccount))
function NamedLimitedAccount:__init(name, limit, initial)
self.NamedAccount:__init(name, initial) -- 'John', 10.00
self.LimitedAccount:__init(limit, initial) -- 0.00, 10.00
end
-- widthdraw() disambiguated to the limit-checking version
function NamedLimitedAccount:withdraw(amount)
return self.LimitedAccount:withdraw(amount)
end
-----------------------------------
myNLAccount = NamedLimitedAccount('John', 0.00, 10.00)
myNLAccount:deposit(2.00)
print('balance now', myNLAccount:getbalance()) --> 12.00
myNLAccount:withdraw(1.00)
print('balance now', myNLAccount:getbalance()) --> 11.00
--myNLAccount:withdraw(15.00) --> error, limit exceeded
Автор: Setuper 20.5.2009, 17:41
Доработки:
1) Добавлена возможность для описания функций и членов класса "внутри" самого класса.
Идентичные примеры:
Код
class.Account()
function Account:__init(initial)
self.balance = initial or 0
end
function Account:deposit(amount)
self.balance = self.balance + amount
end
function Account:withdraw(amount)
self.balance = self.balance - amount
end
function Account:getbalance()
return self.balance
end
Код
class.Account {
__init = function(self, initial)
self.balance = initial or 0
end;
deposit = function(self, amount)
self.balance = self.balance + amount
end;
withdraw = function(self, amount)
self.balance = self.balance - amount
end;
getbalance = function(self)
return self.balance
end;
}
Правила для объявления классов:
Код
class.MyClass([inherited,] [{members}])
Код
[local] MyClass = class(["MyClass",] [inherited,] [{members}])
2) Изменено название функции shared. Новое название: virtual. Думаю, что новое название более логично, а старое может только запутать. Данная функция служит для построения виртуальных классов, и она в точности реализует механизм виртуального наследования классов, как это происходит в языке С++.
3) Для того, чтобы таблица class приобрела свойства зарезервированного слова, запрещена запись в эту таблицу. Данная таблица используется исключительно для построения классов.
4) Исправлены все внутренние поля и локальные переменные с shared на is_virtual, дабы не вносить непонятности.
class.lua ( 12.85 килобайт )
: 99
Автор: Setuper 7.6.2009, 18:40
Примеры использования классов:
(Быстро накатал - не судите строго)
Код
class.cutility {
SomeFunction = function(self)
end;
}
class.cbase {
}
class.cplagin (cutility, cbase, {
name;
desc;
__init = function(self, sName, sDesc)
self.name = sName
self.desc = sDesc
end;
})
--создание объектов класса
mPlagin = cplagin("myFirstPlagin", "ForTest") -- вызов конструктора __init
Существую классы, в которых мы можем описывать то, что нам нужно и существуют объекты, которые строятся на основании классов.
Свой класс можно создать двумя способами:
1)
Код
class.NameOfClass ([<наследуемый_класс>, <наследуемый_класс>, ..., ]{таблица с элементами класса})
в [скобках] - необязательные параметры
2)
Код
NameOfClass = class(["NameOfClass", ][<наследуемый_класс>, <наследуемый_класс>, ... , ]{таблица с элементами класса})
в данном примере класс cplagin наследует классы cutility и cbase. Поэтому в классе cplagin мы можем вызывать функции классов cutility и cbase.
Да, совсем забыл сказать, что для использования классов нужно подключить файл class.lua, который выложен в топике: http://mydc.ru/index.html?showtopic=1429&view=findpost&p=15699
Файл кладётся в папку libs (либо в папку с ptokax.exe). Подключается так:
Код
require"class"
Такое подключение файла в lua аналогично подключению заголовочный файлов в с++
Автор: Setuper 10.6.2009, 15:55
Глобальные функции и переменные модуля class.lua
Глобальные переменные:
class - "конструктор" класса. Переменная, отвечающая за создание класса.
Два эквивалентных метода создания простого класса:
1)
Код
class.MyClass{}
2)
Код
MyClass = class("MyClass", {})
Глобальные функции:virtual(class) - функция, превращающая обычного класса в виртуальный класс.
typeof(value) - расширенная функция определения типа переменной. Возвращает "class", если переменная является классом, "virtual_class" - если переменная представляет из себя виртуальный класс, "object" - если переменная является объектом какого-то класса, а во всех остальных случаях возвращает lua тип переменной, то есть работает как обычная lua функция type().
classof(value) - функция возвращает переменную, если она является классам, в противном случае возвращает nil.
classname(value) - функция возвращает имя класса, если переменная является именованым классом, иначе возвращает nil.
implements(value, class) - функция проверяет, что объект или класс (value) поддерживает интерфейс целевого класса (class). Это означает, что объект или класс может быть подставлен в качестве аргумента в функцию, которая ожидает целевой класс. Мы рассматриваем только элементы класса, которые могут вызываться из класса, то есть вызываемые элементы (функции и таблицы).
is_a(value, class) - функция проверяет содержится ли указанный объект или класс (value), в целевом классе (class). Проверка начинается с самого объекта (или класса), и идёт по цепочке наследования вглубь до целевого класса. Если целевой класс найден, то происходит проверка поддержки интерфейсов (данная функция может вернуть false в механизме множественного наследования, из-за неоднозначностей).
Автор: serg_58 20.10.2013, 15:35
Огромная благодарность за предоставленый материал
Автор: xnt 9.9.2014, 12:19
Большое спасибо за материал. Оказался очень полезен.
Автор: MIKHAIL 1.7.2016, 9:24
Очень бы хотелось уловить суть ООП на примерах скриптов с минимальным функционалом, но реализованным через ООП, для той же Птоки, например.
Если я правильно выражаюсь:
1) в одном скрипте "показать" (сделать упор) на инкапсуляцию;
2) в другом – на наследование;
3) в третьем – на полиморфизм;
4) ну и наконец – всё вместе.
А так, для меня, например, это какая-то абстракция, а как к ней подступиться – неизвестно.
P.S.: не похерьте http://mydc.ru/topic1958.html, а лучше прикрепите её к данной.