Версия для печати темы

Нажмите сюда для просмотра этой темы в обычном формате

MyDC.ru _ Программирование на Lua _ Объектно-Ориентированное Программирование в Lua

Автор: Setuper 13.12.2008, 15:01

Объектно-Ориентированное Программирование (ООП) в LUA

В данной теме обсуждаются мощнейшие методы ООП big_smile.gif


Итак, потихоньку буду писать основные понятия.

Объект и класс

Прежде всего следует понять что такое объект, и что такое класс

Согласно языку С++, класс - это определяемый пользователем тип, а объект - это экземпляр класса.
Попробуем в 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, а лучше прикрепите её к данной.