Объектно-ориентированное программирование на Lua
Объектно-ориентированное программирование (ООП) — очень популярная архитектура компьютерного программирования, которая проектирует приложения путём создания объектов и манипулирования ими.
Все следующие языки программирования поддерживают объектно-ориентированное программирование:
- C++
- Java
- Objective-C
- Smalltalk
- C#
- Ruby
Lua — легковесный скриптовый язык. Хотя в нём нет встроенных мощных объектно-ориентированных (ОО) возможностей, как в Java или C++, он очень гибок и позволяет реализовать объектно-ориентированное программирование с помощью некоторых приёмов.
Объектно-ориентированные возможности
Инкапсуляция: объединение данных и методов, сокрытие деталей реализации, предоставление только необходимых интерфейсов, повышение безопасности и удобства сопровождения.
Наследование: повторное использование и расширение существующего кода путём порождения новых классов, сокращение повторяющегося кода, повышение эффективности разработки и расширяемости.
Полиморфизм: одна и та же операция ведёт себя по-разному для разных объектов, поддерживает унифицированные вызовы интерфейсов и повышает гибкость и расширяемость.
Абстракция: упростить сложные задачи путём определения основных классов и интерфейсов, скрывая ненужные детали для облегчения управления сложностью.
Объектно-ориентированное программирование в Lua
Мы знаем, что объекты состоят из свойств и методов.
Классы в Lua можно смоделировать с помощьюtable + function.
Что касается наследования, его можно смоделировать через метатаблицы (не рекомендуется; моделирования только самых базовых объектов достаточно для большинства реализаций).
В Lua самая базовая структура — это таблица; мы можем использовать таблицы для создания объектов.
ClassName = {} -- 创建一个表作为类
Создавайте объекты с помощью метода new (или других имён) и инициализируйте свойства объекта.
function ClassName:new(...)
local obj = {} -- 创建一个新的空表作为对象
setmetatable(obj, self) -- 设置元表,使对象继承类的方法
self.__index = self -- 设置索引元方法
-- 初始化对象的属性
obj:init(...) -- 可选:调用初始化函数
return obj
end
Таблица — это самый базовый составной тип данных в Lua, и её можно использовать для представления свойств объекта.
Функция в Lua может использоваться для представления методов:
function ClassName:sayHello()
print("Hello, my name is " .. self.name)
end
Используйте метод new для создания объектов и вызывайте методы класса через объект.
local obj = ClassName:new("Alice") -- 创建对象
obj:sayHello() -- 调用对象的方法
В Lua таблицу можно рассматривать как вариант объекта. Как и объекты, таблицы имеют состояние (переменные-члены) и могут представлять независимые сущности.
Таблицы не только имеют элементы данных, но также могут содержать функции-члены, подобные методам объекта:
Пример
Person = {name = "", age = 0}
-- Конструктор Person
function Person:new(name, age)
local obj = {} -- Создать новую таблицу как объект
setmetatable(obj, self) -- Установите метатаблицу, чтобы сделать его экземпляром Person
self.__index = self -- Установите метаметод индекса, чтобы он указывал на Person
obj.name = name
obj.age = age
return obj
end
-- Добавьте метод: вывод личной информации
function Person:introduce()
print("Меня зовут " .. self.name .. " и мне " .. self.age .. " лет.")
end
Пояснение к коду:
- Person— это таблица с двумя свойствами: name и age; эти два свойства являются свойствами класса по умолчанию.
- Person:new(name, age)— это конструктор, используемый для создания новых объектов Person.
- local obj = {}Создайте новую таблицу в качестве объекта; setmetatable(obj, self) устанавливает метатаблицу, так что таблица становится экземпляром класса Person.
- self.__index = selfУстановите метаметод index, чтобы obj мог получать доступ к свойствам и методам класса Person.
- introduceявляется методом класса Person, который выводит имя и возраст объекта Person.
Вызов методов:
-- 创建一个 Person 对象
local person1 = Person:new("Alice", 30)
-- 调用对象的方法
person1:introduce() -- 输出 "My name is Alice and I am 30 years old."
Простой пример
Следующий простой класс содержит три свойства: area, length и breadth. Метод printArea используется для вывода результата вычисления:
Пример
Rectangle = {area = 0, length = 0, breadth = 0}
-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Установите метатаблицу, чтобы self наследовал методы Rectangle
self.__index = self -- Убедитесь, что методы и свойства могут быть найдены при обращении
o.length = length or 0 -- Установите length, по умолчанию 0
o.breadth = breadth or 0 -- Установить ширину, по умолчанию 0
o.area = o.length * o.breadth -- Вычислить площадь
return o
end
-- Вывести площадь прямоугольника
function Rectangle:printArea()
print("Площадь прямоугольника равна ", self.area)
end
Создание объектов
Создание объекта — это процесс выделения памяти для экземпляра класса; каждый класс имеет собственную память и разделяет общедоступные данные:
r = Rectangle:new(nil,10,20)
Доступ к свойствам
Мы можем использовать точку.для доступа к свойствам класса:
print(r.length)
Доступ к функциям-членам
Мы можем использовать двоеточие:для доступа к функциям-членам класса:
r:printArea()
Память выделяется при инициализации объектов.
Полный пример
Ниже мы демонстрируем полный пример объектно-ориентированного программирования на Lua:
Пример
Rectangle = {area = 0, length = 0, breadth = 0}
-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Установите метатаблицу, чтобы self наследовал методы Rectangle
self.__index = self -- Убедитесь, что методы и свойства могут быть найдены при обращении
o.length = length or 0 -- Установите length, по умолчанию 0
o.breadth = breadth or 0 -- Установить ширину, по умолчанию 0
o.area = o.length * o.breadth -- Вычислить площадь
return o
end
-- Вывести площадь прямоугольника
function Rectangle:printArea()
print("Площадь прямоугольника равна ", self.area)
end
-- Запустите пример:
local rect1 = Rectangle:new(nil, 5, 10) -- Создать прямоугольник с длиной 5 и шириной 10
rect1:printArea() -- Вывод "Площадь прямоугольника равна 50"
local rect2 = Rectangle:new(nil, 7, 3) -- Создать прямоугольник с длиной 7 и шириной 3
rect2:printArea() -- Вывод "Площадь прямоугольника равна 21"
Выполнение вышеуказанной программы дает следующий вывод:
矩形面积为 50 矩形面积为 21
Наследование в Lua
Наследование означает, что объект напрямую использует свойства и методы другого объекта и может использоваться для расширения свойств и методов базового класса.
Наследование в Lua реализуется путём установки метатаблицы подкласса.
Мы можем создать новую таблицу и установить в качестве её метатаблицы родительский класс.
В следующем примере класс Square унаследует свойства и методы класса Rectangle и внесёт изменения на этой основе.
Rectangle = {area = 0, length = 0, breadth = 0}
-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Установите метатаблицу, чтобы self наследовал методы Rectangle
self.__index = self -- Убедитесь, что методы и свойства могут быть найдены при обращении
o.length = length or 0 -- Установите length, по умолчанию 0
o.breadth = breadth or 0 -- Установить ширину, по умолчанию 0
o.area = o.length * o.breadth -- Вычислить площадь
return o
end
-- Вывести площадь прямоугольника
function Rectangle:printArea()
print("Площадь прямоугольника равна ", self.area)
end
-- Определить класс Square, наследующий от класса Rectangle
Square = Rectangle:new() -- Square наследует класс Rectangle
-- Переопределить конструктор (все стороны квадрата равны)
function Square:new(o, side)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Установите метатаблицу, чтобы self наследовал методы Rectangle
self.__index = self -- Убедитесь, что методы и свойства могут быть найдены при обращении
o.length = side or 0 -- Установить длину стороны
o.breadth = side or 0 -- Ширина и длина квадрата равны
o.area = o.length * o.breadth -- Вычислить площадь
return o
end
-- Запустите пример:
local rect = Rectangle:new(nil, 5, 10) -- Создать прямоугольник с длиной 5 и шириной 10
rect:printArea() -- Вывести "Площадь прямоугольника равна 50"
local square = Square:new(nil, 4) -- Создать квадрат с длиной стороны 4
square:printArea() -- Вывести "Площадь прямоугольника равна 16"
RectangleКласс: по-прежнему базовый класс Rectangle, имеющийlength、breadthиareaсвойства, а также методы для вычисления и вывода площади.
SquareКласс наследует отRectangle:SquareКласс используетRectangle:new()наследоватьRectangleметоды и свойства класса. Поскольку длина и ширина квадрата равны, мыSquare:newпереопределили конструктор в методе, устанавливаяlengthиbreadthустановлено в одно и то же значение (т.е.side)。
Переопределение конструктора:Square:new(o, side)Когда метод создаёт объект Square, он использует переданную длину стороныsideчтобы инициализироватьlengthиbreadthсвойства и вычисляет площадь.
Результат выполнения:
矩形面积为 50 矩形面积为 16
Переопределение функций
В Lua переопределение функций (также называемое переопределением методов) относится к переопределению или замене существующего метода родительского класса подклассом при наследовании.
Подкласс может по мере необходимости изменять или расширять поведение методов родительского класса.
В приведённом выше примере класс Square переопределяет конструктор класса Rectangle, тем самым изменяя способ инициализации объектов, в частности устанавливая длину и ширину прямоугольника в одно и то же значение, потому что характерная особенность квадрата — равные длины сторон.
Далее мы используем класс Animal и класс Dog, который наследуется от него, чтобы продемонстрировать, как переопределять методы.
Animal = {name = "Unknown"}
-- Конструктор класса Animal
function Animal:new(o, name)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Устанавливаем метатаблицу, чтобы объект наследовал методы Animal
self.__index = self -- Позволяет объекту обращаться к методам Animal
o.name = name or "Unknown" -- Устанавливаем name, значение по умолчанию — "Unknown"
return o
end
-- Метод класса Animal: издавать звук
function Animal:speak()
print(self.name .. " makes a sound.")
end
-- Определяем класс Dog, который наследует Animal
Dog = Animal:new() -- Dog наследует класс Animal
-- Переопределяем конструктор класса Dog
function Dog:new(o, name, breed)
o = o or {} -- Если объект не передан, создайте новую пустую таблицу
setmetatable(o, self) -- Установить метатаблицу, чтобы объект наследовал методы от Dog и Animal
self.__index = self -- Разрешить объекту доступ к методам Dog
o.name = name or "Неизвестно"
o.breed = breed or "Неизвестно"
return o
end
-- Переопределить метод sound класса Dog (переопределяя метод speak класса Animal)
function Dog:speak()
print(self.name .. " лает.")
end
-- Создать объект Animal
local animal = Animal:new(nil, "Обычное животное")
animal:speak() -- Вывод: "Обычное животное издаёт звук."
-- Создать объект Dog
local dog = Dog:new(nil, "Buddy", "Golden Retriever")
dog:speak() -- Вывод "Buddy barks."
AnimalClass: определяет базовый классAnimal, который имеетnameсвойства иspeakметоды.speakМетод — это реализация по умолчанию, которая выводит "some animal makes a sound."DogНаследование классовAnimal:DogКласс наследует отAnimal, и с помощьюDog:new()метод создаёт свои собственные экземпляры.Переопределение
speakметод: вDogкласс, переопределилspeakметод, изменяя его поведение с "издавать звук" родительского класса на "лай собаки". Это проявление переопределения методов. Подкласс (Dog) изменил родительский класс (Animal) поведение метода.
Результат выполнения:
Generic Animal makes a sound. Buddy barks.
Полиморфизм
Полиморфизм в Lua реализуется через метатаблицы и переопределение методов. Когда объекты разных типов вызывают один и тот же метод, Lua выполняет разные методы в зависимости от фактического типа объекта.
Пример
Person = {}
-- Добавить конструктор в "класс"
function Person:new(name, age)
local obj = {} -- Создать новую таблицу как объект
setmetatable(obj, self) -- Установить метатаблицу, чтобы указать, что это экземпляр класса Person
self.__index = self -- Установите метаметод индекса, чтобы он указывал на Person
obj.name = name
obj.age = age
return obj
end
-- Добавить методы
function Person:greet()
print("Привет, меня зовут " .. self.name)
end
-- Определить подкласс Student, который наследуется от Person
Student = Person:new()
-- Подкласс переопределяет метод родительского класса
function Student:greet()
print("Привет, я студент, и меня зовут " .. self.name)
end
local person2 = Person:new("Charlie", 25)
local student2 = Student:new("David", 18)
-- Полиморфизм: объекты разных типов вызывают один и тот же метод
person2:greet() -- Вывод "Привет, меня зовут Charlie"
student2:greet() -- Вывод "Привет, я студент, и меня зовут David"
Хотя person2 и student2 вызывают один и тот же метод greet, Lua вызывает соответствующую версию для каждого, потому что их типы различаются.
Результат выполнения:
Hello, my name is Charlie Hi, I'm a student and my name is David
Другие концепции объектно-ориентированного программирования
Инкапсуляция
Инкапсуляция обычно реализуется путём инкапсуляции данных и методов в таблице. Мы можем имитировать инкапсуляцию, контролируя доступ к таблице, например, используя метаметоды, чтобы ограничить внешний доступ.
Пример
Person = {}
-- Добавить инкапсуляцию: скрыть свойства
function Person:new(name, age)
local obj = {}
setmetatable(obj, self)
self.__index = self
obj.name = name
obj.age = age
return obj
end
function Person:setName(name)
self.name = name -- Предоставить метод для изменения name
end
function Person:getName()
return self.name -- Предоставить метод для получения name
end
Таким образом, мы можем контролировать доступ к свойствам и имитировать инкапсуляцию.
Абстракция
Абстракция означает упрощение сложных вещей путём сокрытия ненужных деталей. Хотя в самом Lua нет концепции классов, мы можем достичь абстракции через инкапсуляцию.
Пример
function Person:showInfo()
print("Name: " .. self.name)
print("Age: " .. self.age)
end