- 72
- 192
Введение.
Всем хай, здрасьте, привет и здарова. Никак не доходили руки рассказать про метатаблицы в Lua и их правильное использование. Чтож, в этой теме вы увидите:
1. Создание класса и его экземпляра, используя метатаблицы.
2. Создание методов, взаимодействующих с данным классом.
3. Перегрузка операторов +, - (в том числе урнарный), *, /, #, *. tostring и прочих.
5. Создание C-подобного конструктора через метаметод __call
1. Создание класса и его экземпляра, используя метатаблицы.
2. Создание методов, взаимодействующих с данным классом.
3. Перегрузка операторов +, - (в том числе урнарный), *, /, #, *. tostring и прочих.
5. Создание C-подобного конструктора через метаметод __call
Создание простого класса.
Единственное, что нужно понимать - все классы будут использоваться на метатаблицах. Отличие от класса и его экземпляра в том, что класс - лишь "заготовка" для того, чтобы создать и в дальнейшем использовать уже его экземпляр.
Приведу пример: vector - класс, vector( 1, 2, 3 ) ( Под 1, 2, 3 я имею ввиду уже значение этого вектора ) - уже экземпляр. Объяснение может быть немного непонятным, но в дальнейшем вы поймете разницу.
Классы мы будем создавать на примере вектора. Это некий объект, который имеет в себе 3 координаты: x, y, z и методы, с помощью которых мы взаимодействуем с этими координатами.
Начать стоит с того, что для класса мы будем использовать обычную таблицу, которую мы в будущем укажем как метатаблицу.
Хорошо, таблицу мы создали. Пора сделать конструктор для того, чтобы мы могли создавать экземпляры класса.
Лично я привык делать его через метод таблицы new. Сделаем так же, только первым аргументом нам нужно будет передать self, а следующими те - которые будут задействованы в объекте.
Или же более компактный вариант
Напомню, что когда мы создаем функцию через :func_name( ... ), то первым аргументом всегда передается self, что будет являться референсом на таблицу, которая содержит этот метод.
Лично я использую первый вариант, тут уж, дело вкуса.
Хорошо, теперь в конструкторе нам нужно создать еще одну таблицу, которая будет хранить в себе значения всех аргументов, которые мы передали в конструктор.
Обратите внимание на то, что я использую vector.x = x or 0.0 и т.д. Это делается для того, чтобы указать аргументы как необязательные. То-есть если человек захочет нулевой вектор (вектор, у которого все координаты равны нулю), то ему не придется указывать их вручную. Так же происходит и с 2-х мерным вектором, пользователю банально не придется указывать Z координату как 0.0, ему достаточно будет указать только X и Y
Теперь нам нужно зарегистрировать нашу внутреннюю таблицу как класс, делается это таким образом:
Вкратце про __index = self. __index это такой метаметод, который вызывается, если в текущей таблице не найдено значение, которое индексируется.
То-есть если имеется таблица AAA и BBB, но в таблицу BBB __index указан как таблица AAA, то при попытке получить значение, которого нет в BBB, Lua попытается обратиться к таблице AAA и найти там значение. Вот так вот.
__index может быть не только ссылкой на таблицу, а еще и функцией, которая обрабатывает тот ключ, который вы пытаетесь получить и возвращает результат, который указали вы.
В данном случае мы указали __index = self (где self - ссылка на внешнюю таблицу vector) для того, чтобы позже создать методы, которые будут описаны не во внутренней таблице, а где то вне.
Приведу пример: vector - класс, vector( 1, 2, 3 ) ( Под 1, 2, 3 я имею ввиду уже значение этого вектора ) - уже экземпляр. Объяснение может быть немного непонятным, но в дальнейшем вы поймете разницу.
Классы мы будем создавать на примере вектора. Это некий объект, который имеет в себе 3 координаты: x, y, z и методы, с помощью которых мы взаимодействуем с этими координатами.
Начать стоит с того, что для класса мы будем использовать обычную таблицу, которую мы в будущем укажем как метатаблицу.
Lua:
local vector = { }
Лично я привык делать его через метод таблицы new. Сделаем так же, только первым аргументом нам нужно будет передать self, а следующими те - которые будут задействованы в объекте.
Lua:
vector.new = function( self, x, y, z )
end
Lua:
function vector:new( x, y, z )
end
Лично я использую первый вариант, тут уж, дело вкуса.
Хорошо, теперь в конструкторе нам нужно создать еще одну таблицу, которая будет хранить в себе значения всех аргументов, которые мы передали в конструктор.
Lua:
vector.new = function( self, x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
end
Теперь нам нужно зарегистрировать нашу внутреннюю таблицу как класс, делается это таким образом:
Lua:
vector.new = function( self, x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
return setmetatable( vector, {
__index = self
} )
end
То-есть если имеется таблица AAA и BBB, но в таблицу BBB __index указан как таблица AAA, то при попытке получить значение, которого нет в BBB, Lua попытается обратиться к таблице AAA и найти там значение. Вот так вот.
__index может быть не только ссылкой на таблицу, а еще и функцией, которая обрабатывает тот ключ, который вы пытаетесь получить и возвращает результат, который указали вы.
В данном случае мы указали __index = self (где self - ссылка на внешнюю таблицу vector) для того, чтобы позже создать методы, которые будут описаны не во внутренней таблице, а где то вне.
Создание методов класса.
Для начала поясним, чем метод отличается от обычной функции. Метод - это функция, в которую первым аргументом всегда передается self (или this в C-подобных языках). А функция - все остальные виды функций.
Пояснение еще проще - методы присутствуют только у классов и структур.
И так, для создание методов мы будем использовать уже ВНЕШНЮЮ таблицу vector.
Вот как это выглядит:
Длина вектора высчитывается по формуле: sqrt( x^2 + y^2 + z^2 ), это мы и опишем в данном методе.
Теперь поясню откуда взялся self и что он есть на самом деле. Немного выше я уже говорил про __index, так вот:
Из-за того, что мы указали в конструкторе __index = self (self - внешняя vector таблица), то при попытке найти метод length во внутренней таблице, из-за того, что там ее нет Lua обратиться ко внешней таблице, где этот метод и лежит и затем вызовет его.
Давайте добавим еще пару методов:
Метод clone возвращает новый вектор с такими же координатами, а dist - дистанция текущего вектора до вектора, переданного в параметры.
Пояснение еще проще - методы присутствуют только у классов и структур.
И так, для создание методов мы будем использовать уже ВНЕШНЮЮ таблицу vector.
Вот как это выглядит:
Lua:
vector.new = function( self, x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
return setmetatable( vector, {
__index = self
} )
end
vector.length = function( self )
end
Lua:
vector.length = function( self )
return math.sqrt( math.pow( self.x, 2 ) + math.pow( self.y, 2) + math.pow( self.z, 2 ) )
end
Из-за того, что мы указали в конструкторе __index = self (self - внешняя vector таблица), то при попытке найти метод length во внутренней таблице, из-за того, что там ее нет Lua обратиться ко внешней таблице, где этот метод и лежит и затем вызовет его.
Давайте добавим еще пару методов:
Lua:
vector.clone = function( self )
return vector.new( self.x, self.y, self.z )
end
vector.dist = function( self, v_other )
return math.sqrt(
math.pow( v_other.x - self.x, 2 ) + math.pow( v_other.y - self.y, 2 ) + math.pow( v_other.z - self.z, 2 )
)
end
Перегрузка операторов.
Перегрузка операторов это ни что иное, как изменение поведения и результата при определенных операторах.
Например мы можем сделать так, чтобы при складывании двух чисел они перемножались, при делении - вычитались и т.д.
Давайте вернемся к первому пункту и немного перепишем код:
1. Уберем self первым параметром из vector.new (Дальше будет объяснено почему)
2. Заменим таблицу, передаваемую вторым параметром в setmetatable на новую, указав ссылку на нее вместо явного определения
3. Укажем __index = vector, для того, чтобы не потерять взаимодействия с методами, т.к. self у нас больше не имеется.
В итоге получаем такой код:
Для чего это сделано? Для того, чтобы перегрузить операторы, нам нужно задать их реализацию непосредственно в таблице с данными (внутренней).
Это все из-за того, что внешняя таблица vector не является метатаблицы для внутренней.
Да, это конечно можно сделать подобным образом:
Но это, как по мне, сделает более грязным. Это чисто дело вкуса, каждый дрочит как он хочет.
Чтож, вернемся к перегрузке операторов.
Для ее реализации нам понадобятся следующие метаметоды:
__add - эквивалент ( + ) сложению
__sub - эквивалент ( - ) вычитанию
__unm - эквивалент ( -num ) урнарному минусу
__mul - эквивалент ( * ) умножению
__div - эквивалент ( / ) делению
__len - эквивалент ( # ) оператору длины в Lua
А так же метаметод __tostring. Он используется при приведению какого-либо типа данных в строку. Как его использовать поясню чуть ниже.
И так, возвращаемся в нашу новую таблицу vector_mt и пишем следующее:
По стандарту перегрузки операторов должны возвращать новый экземпляр класса для того, чтобы иметь возможность выстраивать цепочку математических операторов.
Описываем логику для каждого метаметода.
Таким образом мы перегрузили операторы. Теперь при вызове vector + vector2 нам вернется сумма соответствующих координат в новом векторе, а при попытке узнать длину вектора через # нам вернется его длина.
Так же сделаем перегрузку tostring. Сделаем так, что при попытке привести вектор к строке нам вернется его конструктор (который мы сделаем чуть позже)
Например мы можем сделать так, чтобы при складывании двух чисел они перемножались, при делении - вычитались и т.д.
Давайте вернемся к первому пункту и немного перепишем код:
1. Уберем self первым параметром из vector.new (Дальше будет объяснено почему)
2. Заменим таблицу, передаваемую вторым параметром в setmetatable на новую, указав ссылку на нее вместо явного определения
3. Укажем __index = vector, для того, чтобы не потерять взаимодействия с методами, т.к. self у нас больше не имеется.
В итоге получаем такой код:
Lua:
local vector_mt = { } do
vector.mt.__index = vector
end
vector.new = function( x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
return setmetatable( vector, vector_mt )
end
Это все из-за того, что внешняя таблица vector не является метатаблицы для внутренней.
Да, это конечно можно сделать подобным образом:
Lua:
local vector = { } do
vector.__index = vector
vector.new = function( self, x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
return setmetatable( vector, self )
end
end
Чтож, вернемся к перегрузке операторов.
Для ее реализации нам понадобятся следующие метаметоды:
__add - эквивалент ( + ) сложению
__sub - эквивалент ( - ) вычитанию
__unm - эквивалент ( -num ) урнарному минусу
__mul - эквивалент ( * ) умножению
__div - эквивалент ( / ) делению
__len - эквивалент ( # ) оператору длины в Lua
А так же метаметод __tostring. Он используется при приведению какого-либо типа данных в строку. Как его использовать поясню чуть ниже.
И так, возвращаемся в нашу новую таблицу vector_mt и пишем следующее:
Lua:
vector_mt.__add = function( self, v_other )
end
vector_mt.__sub = function( self, v_other )
end
vector_mt.__unm = function( self )
end
vector_mt.__mul = function( self, v_other )
end
vector_mt.__div = function( self, v_other )
end
vector_mt.__len = function( self )
end
Описываем логику для каждого метаметода.
Lua:
vector_mt.__add = function( self, v_other )
return vector.new(
self.x + v_other.x, self.y + v_other.y, self.z + v_other.z
)
end
vector_mt.__sub = function( self, v_other )
return vector.new(
self.x - v_other.x, self.y - v_other.y, self.z - v_other.z
)
end
vector_mt.__unm = function( self )
return vector.new( -self.x, -self.y, -self.z )
end
vector_mt.__mul = function( self, v_other )
return vector.new(
self.x * v_other.x, self.y * v_other.y, self.z * v_other.z
)
end
vector_mt.__div = function( self, v_other )
return vector.new(
self.x / v_other.x, self.y / v_other.y, self.z / v_other.z
)
end
vector_mt.__len = function( self )
return self:length( )
end
Так же сделаем перегрузку tostring. Сделаем так, что при попытке привести вектор к строке нам вернется его конструктор (который мы сделаем чуть позже)
Lua:
vector_mt.__tostring = function( self )
return string.format( "vector( %.1f, %.1f, %.1f )", self.x, self.y, self.z )
end
Создание C-подобного конструктора.
Окей, для начала поясню, что такое C-подобный конструктор. Например, в C++ он выглядит так:
Vector vec1 = Vector( 1.0f, 1.2f, 1.3f );
То-есть мы явно не вызываем метод new, а сразу обращаемся с вызовом к таблице vector.
Реализуется он с помощью метаметода __call, который срабатывает когда таблицу пытаются вызвать как функцию.
Сделаем это так:
Сделаем метатаблицей внешнюю таблицу vector, которой определим метод __call:
Обратите внимание на то, что мы передаем self первым аргументом, т.к. каждый метаметод обязан иметь таковой.
И теперь со спокойной душой мы можем протестировать все это дело:
Полный код:
Vector vec1 = Vector( 1.0f, 1.2f, 1.3f );
То-есть мы явно не вызываем метод new, а сразу обращаемся с вызовом к таблице vector.
Реализуется он с помощью метаметода __call, который срабатывает когда таблицу пытаются вызвать как функцию.
Сделаем это так:
Сделаем метатаблицей внешнюю таблицу vector, которой определим метод __call:
Lua:
setmetatable( vector, {
__call = function( self, x, y, z )
return vector.new( x, y, z )
end
} )
И теперь со спокойной душой мы можем протестировать все это дело:
Lua:
local vec1 = vector( 1, 2, 3 )
local vec2 = vector( 2, 3, 4 )
print( "first vector: ", vec1 )
print( "second vector: ", vec2 )
local sum = vec1 + vec2
print( "sum of 2 vectors: ", sum )
local sub = vec1 - vec2
print( "sub of 2 vectors: ", sub )
local mul = vec1 * vec2
print( "mul of 2 vectors: ", mul )
local div = vec1 / vec2
print( "div of 2 vectors: ", div )
local length = #vec1
print( "length of vec1: ", length )
local length2 = vec2:length( )
print( "length of vec2: ", length2 )
Полный код:
Lua:
local vector = { } do
local vector_mt = { } do
vector_mt.__index = vector
vector_mt.__add = function( self, v_other )
return vector.new(
self.x + v_other.x, self.y + v_other.y, self.z + v_other.z
)
end
vector_mt.__sub = function( self, v_other )
return vector.new(
self.x - v_other.x, self.y - v_other.y, self.z - v_other.z
)
end
vector_mt.__mul = function( self, v_other )
return vector.new(
self.x * v_other.x, self.y * v_other.y, self.z * v_other.z
)
end
vector_mt.__div = function( self, v_other )
return vector.new(
self.x / v_other.x, self.y / v_other.y, self.z / v_other.z
)
end
vector_mt.__len = function( self )
return self:length( )
end
vector_mt.__tostring = function( self )
return string.format( "vector( %.1f, %.1f, %.1f )", self.x, self.y, self.z )
end
end
vector.new = function( x, y, z )
local vector = { }
vector.x = x or 0.0
vector.y = y or 0.0
vector.z = z or 0.0
return setmetatable( vector, vector_mt )
end
vector.length = function( self )
return math.sqrt( math.pow( self.x, 2 ) + math.pow( self.y, 2) + math.pow( self.z, 2 ) )
end
vector.clone = function( self )
return vector.new( self.x, self.y, self.z )
end
vector.dist = function( self, v_other )
return math.sqrt(
math.pow( v_other.x - self.x, 2 ) + math.pow( v_other.y - self.y, 2 ) + math.pow( v_other.z - self.z, 2 )
)
end
setmetatable( vector, {
__call = function( self, x, y, z )
return vector.new( x, y, z )
end
} )
end
local vec1 = vector( 1, 2, 3 )
local vec2 = vector( 2, 3, 4 )
print( "first vector: ", vec1 )
print( "second vector: ", vec2 )
local sum = vec1 + vec2
print( "sum of 2 vectors: ", sum )
local sub = vec1 - vec2
print( "sub of 2 vectors: ", sub )
local mul = vec1 * vec2
print( "mul of 2 vectors: ", mul )
local div = vec1 / vec2
print( "div of 2 vectors: ", div )
local length = #vec1
print( "length of vec1: ", length )
local length2 = vec2:length( )
print( "length of vec2: ", length2 )
Последнее редактирование: