Как-то я всерьез занялся покорением ООП в lua. И все, что я находил в интернете по этой теме, было вырвиглазными нагромождениями кода с обилием нижних подчеркиваний, которые никак не вписывались в элегантность этого языка. Поэтому я решил искать простое решение.
После прочтения множества умных книжек и разбора нескольких ужасных реализаций ООП, я, крупица за крупицей, собирал все самое полезное и простое, пока не выработал свой стиль объектно ориентированного программирования на lua.
Создание класса и экземпляра
--класс
Person= {}
--тело класса
function Person:new(fName, lName)
-- свойства
local obj= {}
obj.firstName = fName
obj.lastName = lName
-- метод
function obj:getName()
return self.firstName
end
--чистая магия!
setmetatable(obj, self)
self.__index = self; return obj
end
--создаем экземпляр класса
vasya = Person:new("Вася", "Пупкин")
--обращаемся к свойству
print(vasya.firstName) --> результат: Вася
--обращаемся к методу
print(vasya:getName()) --> результат: Вася
Как видите, все очень просто. Если кто-то путается где ставить точку, а где двоеточие, правило следующее: если обращаемся к свойству — ставим точку (object.name), если к методу — ставим двоеточие (object:getName()).
Дальше интереснее.
Как известно, ООП держится на трех китах: наследование, инкапсуляция и полиморфизм. Проведем «разбор полетов» в этом же порядке.
Наследование
Допустим, нам нужно создать класс унаследованный от предыдущего (Person).
Woman = {}
--наследуемся
setmetatable(Woman ,{__index = Person})
--проверяем
masha = Woman:new("Марья","Ивановна")
print(masha:getName()) --->результат: Марья
Все работает, но лично мне не нравится такой вариант наследования, некрасиво. Поэтому я просто создаю глобальную функцию extended():
function extended (child, parent)
setmetatable(child,{__index = parent})
end
Теперь наследование классов выглядит куда красивее:
Woman = {};
--наследуемся
extended(Woman, Person)
--проверяем
masha = Woman:new("Марья","Ивановна")
print(masha:getName()) --->результат: Марья
Инкапсуляция
Все свойства и методы до этого момента в наших классах были публичные, но мы так же легко можем создавать и приватные:
Person = {}
function Person:new(name)
local private = {}
--приватное свойство
private.age = 18
local public = {}
--публичное свойство
public.name = name or "Вася" -- "Вася" - это значение по умолчанию
--публичный метод
function public:getAge()
return private.age
end
setmetatable(public,self)
self.__index = self; return public
end
vasya = Person:new()
print(vasya.name) --> результат: Вася
print(vasya.age) --> результат: nil
print(vasya:getAge()) --> результат: 18
Видите? Все почти так же как вы и привыкли.
Полиморфизм
Тут все еще проще.
Person = {}
function Person:new(name)
local private = {}
private.age = 18
local public = {}
public.name = name or "Вася"
--это защищенный метод, его нельзя переопределить
function public:getName()
return "Person protected "..self.name
end
--это открытый метод, его можно переопределить
function Person:getName2()
return "Person "..self.name
end
setmetatable(public,self)
self.__index = self; return public
end
--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person) --не забываем про эту функцию
--переопределим защищенный метод
function Woman:getName()
return "Woman protected "..self.name
end
--переопределим метод getName2()
function Woman:getName2()
return "Woman "..self.name
end
--проверим
masha = Woman:new()
print(masha:getName()) --> Person protected Вася
print(masha:getName2()) --> Woman Вася
Итак, что мы тут сделали?
— создали класс Person, с двумя методами: getName() и getName2(), первый из них защищен от переопределения;
— создали класс Woman и унаследовали его от класса Person;
— переопределили оба метода в классе Woman. Первый не переопределился;
— получили профит!
Кстати, открытые методы можно определять так же и вне тела класса:
Person = {}
function Person:new(name)
local private = {}
private.age = 18
local public = {}
public.name = name or "Вася"
--это защищенный метод, его нельзя переопределить
function public:getName()
return "Person protected "..self.name
end
setmetatable(public,self)
self.__index = self; return public
end
--это открытый метод, его можно
function Person:getName2()
return "Person "..self.name
end
А что делать, если нужно вызвать метод базового класса, который у нас переопределен? Это тоже делается легко!
Синтаксис таков: РодительскийКласс.Метод(сам_объект, параметры (если есть)).
--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person) --не забываем про эту функцию
--переопределим метод setName
function Woman:getName2()
return "Woman "..self.name
end
print(masha:getName2()) --> Woman Вася
--вызываем метод родительского класса
print(Person.getName2(masha)) --> Person Вася
Постскриптум
На этом все, искренне надеюсь, что хоть кому-нибудь эта статья окажется полезной.
Напоследок приведу полный код, можете его скопипастить в IDE и убедиться в работоспособности.
function extended (child, parent)
setmetatable(child,{__index = parent})
end
Person = {}
function Person:new(name)
local private = {}
private.age = 18
local public = {}
public.name = name or "Вася"
--это защищенный метод, его нельзя переопределить
function public:getName()
return "Person protected "..self.name
end
--этот метод можно переопределить
function Person:getName2()
return "Person "..self.name
end
setmetatable(public,self)
self.__index = self;
return public
end
--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person) --не забываем про эту функцию
--переопределим метод setName
function Woman:getName2()
return "Woman "..self.name
end
masha = Woman:new()
print(masha:getName2()) --> Woman Вася
--вызываем метод родительского класса
print(Person.getName2(masha)) --> Person Вася
Комментарии (19)
gcc
01.06.2015 20:52+4Woman = {};
--наследуемся
extended(Woman, Person)
Было бы еще прикольнее писать Woman = extend(Person)
Если кто-то путается где ставить точку, а где двоеточие, правило следующее: если обращаемся к свойству — ставим точку (obj.name), если к методу — ставим двоеточие (obj:getName()).
Чтобы действительно не путать, лучше запомнить смысл двоеточия — это просто синтаксический сахар, который неявно добавляет к функции первый аргумент self. Можно было бы использовать точку, но тогда пришлось бы явно добавлять аргумент самим, чтобы иметь доступ к объекту, для которого вызван метод (документация).
stepanp
01.06.2015 21:24+5Не понимаю плясок с бубном вокруг инкапсуляции и приватных полей в скриптовых языках. Нафига это там нужно, непонятно.
Без этой ерунды, ООП в lua добавляется буквально одной функцией.Mingun
01.06.2015 22:14+2Кроме того, private на самом деле еще более public, чем public, т.к. доступен всем без исключения, что, собственно, функция setName и демонстрирует. Но все еще хуже. Попробуйте создать два класса с приватными полями. Будите сильно удивлены (это я автору).
Lerg
01.06.2015 23:04+3Похоже и мне теперь нужно написать про ООП в Lua. Мой метод не использует setmetatable() и экономит ресурсы.
Кстати у вас глобальные переменные.
barabanus
01.06.2015 23:57Я когда-то делал парсер для текстовых игр на Lua, получилось что-то вроде этого:
sixdays:new "room" () { room = true } sixdays:new "player" () : moveto( room ) sixdays:new "apple_red" () { name = "red apple" } : moveto( room ) sixdays:new "apple_green" (){ name = "green apple" } : moveto( room ) sixdays:new "table_red" () { name = "red table" } : moveto( room ) sixdays:new "table_green" (){ name = "green table" } : moveto( room )
new — создание нового объекта с глобальным именем, указанным в кавычках
в скобках можно указать, от кого наследуемся (список)
в фигурных скобках конструктор
nemilya
02.06.2015 00:51Спасибо, что поделились опытом.
Я давно присматриваюсь к Lua.
V-REP скриптуется на Lua — роботы-Lua)
GeckoPelt
02.06.2015 08:50Если кто не пробовал, рекомендую посмотреть на Squirrel
www.squirrel-lang.org
Очень похоже на Lua, но ООП без костылей.ant00N Автор
02.06.2015 09:58белка хороша, но слишком редка. Я видел всего один игровой движок (а луа я использую именно в них), в котором был squirrel, да и тот умер.
К тому же, луа быстрее.GeckoPelt
02.06.2015 10:59Lua быстрее, но имхо это далеко не везде критично во встроенном скриптовом интерпретаторе.
Кстати, для белки еще есть отличный биндинг SqPlus.
AterCattus
Неделя Lua ООП прямо. А по общему числу постов на эту тему на Хабре хоть сборник собирай.