- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF123:Lua
Материал из Linuxformat.
- Lua Язык программирования сценариев, встраиваемый в ваши приложения
Содержание |
Lua: Функции и объекты
LUA |
---|
|
- Часть 2: Разобравшись с базовыми возможностями Lua, Андрей Боровский пробует эмулировать в нем конструкции, знакомые по другим языкам.
На предыдущем уроке мы узнали о существовании Lua – встраиваемого языка сценариев; мы разобрались, чем он может быть полезен, и рассмотрели примеры написанных на нем простых программ. Мы освоили ввод-вывод и основные управляющие конструкции и познакомились с таблицами – фундаментальным типом данных Lua, лежащим в основе всего мало-мальски сложного (и интересного).
Сегодня мы изучимболее продвинутые возможности Lua, включая реализацию функций объектно-ориентированного программирования (в стандарте языка они отсутствуют). Но сперва изучим один базовый тип данных, не затронутый в прошлый раз.
Функции Lua
Давайте рассмотрим такую коротенькую программу:
function foo() print(“Привет, я - функция foo()!”) return 1 end print(foo()) print(foo)
Первые четыре строки в пояснениях особо не нуждаются. Ключевое слово function объявляет функцию. Далее следуют ее имя и список аргументов, заключенный в скобки (у нас он пуст). Тело функции – это блок, обязанный заканчиваться ключевым словом end. Обратите внимание, что хотя наша функция возвращает значение 1, оператором return, тип возвращаемого значения в ее объявлении не указывается. Аналогично тому, как одна и та же переменная Lua может принимать значения любых определенных в языке типов, одна и та же функция Lua может возвращать значения всех возможных типов. Все-таки Lua не зря назван именем небесного тела, обозначающего в символике многих народов переменчивость и обманчивость. В пятой строке мы распечатываем значение, возвращаемое foo() (при этом, естественно, выполняется сама функция foo()). Шестая строка выглядит интереснее. В нашем фрагменте, foo – это переменная, содержащая значение типа «функция» (на самом деле – идентификатор функции, но об этом ниже). В шестой строке мы печатаем значение переменной foo, а не результат, возвращаемый функцией. Вот что мы получим:
Привет, я - функция foo()! 1 function: 00379B20
Первые две строки вывода – результат выполнения выражения print(foo()). Последняя строка показывает содержимое переменной foo. Слово function свидетельствует о том, что она содержит идентификатор функции. Далее следует само значение идентификатора (в нашем случае – 32‑битное шестнадцатеричное число). Возникает соблазн назвать идентификатор адресом функции, но следует помнить, что концепция адресов и указателей в Lua отсутствует.
Для завершения примера приведем определение функции, которая принимает параметры:
bar = function(a, b) print(“a+b=”.. a + b) end bar(2,3)
Конструкция
bar = function(a, b)
эквивалентна
function bar(a, b)
как, например, в JavaScript. А вот еще один интересный момент:
function baz() return 1, true, “три” end a,b,c = baz() print(a,b,c)
Да, вы правильно поняли – функции Lua могут возвращать несколько значений одновременно, причем они могут быть разных типов. Если ваш преподаватель С++ увидит подобный кусок кода и кинется оборвать вам руки, скажите ему, что вы пишете на Lua, и одну руку, возможно, спасете (вторую он вам все-таки оторвет – за использование интерпретируемых языков).
Итераторы
Скажи я вам, что в Lua нельзя объявить функцию с переменным числом параметров, вы бы наверняка удивились. Увы, удивить мне вас нечем: такие функции в Lua существуют:
function sum(...) r = 0 for i, v in ipairs(arg) do r = r + v end return r end print(sum(1,2,4,8,16,32))
В этом примере много новых элементов. Троеточие в заголовке функции означает, что число принимаемых аргументов может быть любым. Для передачи переменного числа аргументов используются таблицы, которые, напомню, представляют собой основу всех сложных типов данных в Lua. Увидев троеточие, интерпретатор Lua автоматически создает таблицу arg, содержащую пары «номер – значение аргумента». Нумерация аргументов начинается с единицы и продолжается непрерывно, так что выражение arg[1] возвращает первый аргумент, arg[2] – второй, и т. д.
Вспомнив определение оператора # (LXF122), вы поймете, что выражение #arg вернет число аргументов функции. Однако разработчикам Lua этого показалось мало, и в таблице arg есть еще одно поле с индексом n, которое содержит число аргументов, так что вместо #arg можно (и предпочтительно) использовать arg.n.
Зная все это, мы могли бы использовать уже известную нам форму оператора for для работы с численными индексами элементов таблицы arg (предлагаю вам сделать это самостоятельно). Мы же рассмотрим другой вариант, обладающий более широкими возможностями. В общем виде он выглядит так:
for <список переменных> in <итератор, данные> do ... end
Функции-итераторы служат для последовательного перебора элементов таблицы и могут использоваться не только в операторе for. В нашем примере мы используем встроенную функцию ipairs(), которая, будучи совмещена с циклом for, последовательно заполняет две переменные парами значений «индекс аргумента – его значение» (в нашем примере i содержит индекс элемента arg, а v – значение индексированного элемента). В результате переменная v последовательно принимает значения всех аргу-ментов (т. е. элементов таблицы arg). У функции ipairs() есть брат-близнец pairs(), который оперирует парами «ключ–значение», а не «индекс–значение» (см. врезку).
Если в рассмотренном нами примере итератор ipairs() заменить на pairs(), результат выполнения функции sum() будет другим. Дело в том, что ipairs() перебирает только индексируемые элементы массива, тогда как pairs() учтет и arg.n. Значение этого элемента в нашем примере равно 6, так что вместо ожидаемой суммы 63 мы получим 69.
Теперь вам явно хочется написать собственный итератор! Давайте реализуем итератор bpairs(), перебирающий элементы массива arg в обратном порядке. Как ни странно, для этого потребуется объявить не одну, а две функции:
function backwards(table, count) count = count - 1 if table[count] then return count, table[count] end end function bpairs(table) return backwards, table, #table+1 end
Аргументами функции backwards() должны быть таблица table и значение count, равное количеству индексируемых элементов плюс 1. Внутри самой функции значение count уменьшается на 1, и возвращается это уменьшенное значение и соответствующий ему элемент таблицы. Так будет происходить до тех пор, пока table[count] не окажется равным nil. Если вам кажется, что с функцией backwards() не все так просто, читайте врезку.
Параметры-переменные функций Lua передаются не по значению, а по ссылке. Таким образом, изменение значения любого аргумента внутри функции приводит к изменению этого значения и за ее пределами. Этим фактом мы и пользуемся в функции backwards().
Функция bpairs() работает и того проще. Она возвращает три вещи: саму функцию backwards() и значения аргументов для ее первого вызова. Оператор for вызывает функцию backwards(), используя «для затравки» значения, полученные от bpairs(), до тех пор, пока backwards() возвращает результат. Если вы не поняли это место, не пугайтесь: сейчас будет еще один наглядный пример. Теперь мы можем заменить строку
for i, v in ipairs(arg) do
строкой
for i, v in bpairs(arg) do
Аргументы функции sum() будут перебираться в обратном порядке, в чем можно убедиться, вставив в цикл вызов print(i,v). Сам результат от перемены мест слагаемых не изменится.
Зная, как работают функции-итераторы, мы можем воспроизвести механику оператора for и без обертки bpairs():
for i, v in backwards, arg, #arg+1 do r = r + v end
или
for i, v in backwards, arg, arg.n+1 do r = r + v end
В принципе, функция bpairs() нам не нужна. Это просто удобство, позволяющее написать одно выражение вместо трех.
Чего только нет
Как и в любых других блоках, в теле функции можно объявлять локальные переменные, видимые только внутри нее. В отличие от C/C++, эти переменные нельзя объявить статическими, то есть сделать так, чтобы они хранили данные в перерывах между вызовами функции. Впрочем, статические локальные переменные можно эмулировать. Вот одно из возможных решений:
do local loc=0 function fred(a) loc=loc+a return loc end end
Переменная loc объявлена как локальная, и за пределами блока do...end видна не будет. Функция fred(), напротив, не локальная, и ее можно вызывать за пределами блока. Поскольку переменная loc объявлена вне блока функции fred(), она будет существовать в перерывах между вызовами fred(), но поскольку loc локальна для блока, в котором определена функция fred(), никто, кроме fred(), не сможет получить к ней доступ.
Нет в синтаксисе Lua и концепции параметра со значением по умолчанию (как в C++), но и тут нам на помощь приходит хакерская изобретательность:
function defval(v) v = v or 'default value' return v end print(defval()) print(defval(‘Мое значение’))
То, что при объявлении функции указан список параметров, не означает, что соответствующие им значения необходимо вводить при вызове. Если параметру функции не сопоставлено значение, он будет равен nil. Смысл строки
v = v or 'default value'
можно перевести так: если v не равно nil, присвоить v значение v, иначе присвоить v значение 'default value'. Оператор or ведет себя здесь не так, как при работе с логическими значениями, а как краткая форма if. Таким образом, если при вызове defval() мы не указываем v, в теле функции ему назначается значение по умолчанию. В противном случае используется значение, переданное через v.
Поскольку функции, определенные в Lua – это не блоки машинного кода, намертво скомпонованные с основной программой, а структуры данных, предназначенные для интерпретатора, их можно удалять (высвобождая тем самым оперативную память).
Например, строка
backwards = nil
удаляет функцию backwards(). Тут, правда, есть один тонкий момент. Рассмотрим фрагмент
foo = backwards
После первого присваивания идентификатор foo можно использовать так же, как идентификатор backwards. Например:
for i, v in foo, arg, arg.n+1 do
При этом мы не делаем из одной функции две. Как было сказано выше, у каждой определенной нами функции есть численный идентификатор, который и копируется в процессе присваивания. Если теперь мы напишем
backwards = nil
переменная backwards перестанет указывать на функцию, а foo – не перестанет. В результате память, занятая функцией, освобождена не будет. Уследить за тем, чтобы ни одна переменная не содержала идентификатор функции (а только в этом случае произойдет ее удаление) очень сложно. Эту задачу выполняет автоматический сборщик мусора. Контрольный вопрос: при каких условиях сборщик мусора сможет удалить переменную loc из примера с функцией fred()? Ответ: когда будут удалены все ссылки на fred().
Думаю, что за время чтения этого раздела вы получили столько информации о функциях Lua, что ее требуется переварить. Когда процесс закончится, вспомните то, что будет наиболее важным для следующего раздела: численные идентификаторы функций являются простыми значениями, которые могут присваиваться любым переменным, в том числе, элементам таблиц.
Объекты в Lua
Родные объекты в Lua отсутствуют, и нам придется их эмулировать. Гибкость синтаксиса это позволяет, но прежде необходимо понимать основы реализации ООП в других языках. В первом приближении, объект – это совокупность структур данных и методов для оперирования ими. Если структура данных и набор методов у двух объектов совпадают, эти объекты могут принадлежать (а могут и не принадлежать) одному классу. Как правило, в программе используется несколько объектов одного класса. Для каждого из них создается своя область данных (чем же иначе объекты будут отличаться друг от друга?), но для ее обработки у всех объектов одного класса используются (физически) одни и те же методы. Каким образом метод, который мало чем отличается от обычной функции, узнает, с какой именно структурой данных ему предстоит работать? Для этой цели у него есть скрытый параметр (в одних языках он называется this, в других – self, в третьих – dontuseme), который представляет собой указатель на структуру данных того объекта, для которого вызывается метод.
Этих неполных и неформальных понятий нам пока будет достаточно. Педанты могут обратиться к теории ООП, но предупреждаю, что теорий существует несколько, и все они насыщены весьма сложными абстрактными понятиями, взятыми из алгебры и теории множеств.
С учетом изложенного выше, давайте рассмотрим определение объекта Employee (сотрудник).
Employee = {name = , age = 0, salary = 0, position = } function Employee.incAge(self) self.age = self.age + 1 end function Employee.scaleSalary(self, factor) self.salary = self.salary*factor end function Employee.print(self) print(self.name, 'age: '..self.age, 'salary: '..self.salary, 'position: '..self.position) end; Employee.name = 'Vasya Pupkin' Employee.age = 25 Employee.position = 'Manager' Employee.salary = 1000 Employee:incAge() e = Employee Employee = nil e:scaleSalary(2); e:print()
У объекта (таблицы) Employee есть четыре поля данных, имена которых говорят сами за себя. Кроме того, для объекта Employee определено три метода: incAge(), увеличивающий значение поля age на единицу, scaleSalary(), умножающий поле salary на заданный коэффициент (желательно – больший единицы) и print(), выводящий сведения о сотруднике.
Обращаю ваше внимание на то, что конструкция
function Employee.print(self)
эквивалентна
Employee.print = function (self)
Мы просто создаем еще один элемент таблицы Employee со значением типа «функция». Практически все элементы синтаксиса в представленном фрагменте вам уже знакомы. Новшеством является только выражение типа
Employee:incAge()
Оператор : означает, что первый аргумент вызываемой функции – ссылка на таблицу, имя которой расположено слева от оператора (напомню, все переменные-параметры в Lua передаются по ссылке). В данном случае, без : можно и обойтись, написав
Employee.incAge(Employee)
Однако такая форма записи более громоздка и не всегда верна: например, она не сработает при использовании полиморфизма.
Оператор : применим не только при вызове, но и при объявлении методов. Например, вместо
function Employee.print(self)
можно написать
function Employee:print()
A вместо
Employee.setName(self, name)
использовать
Employee::setName(name)
Параметр self для функций будет создан автоматически.
Рассмотрим четыре последних строки программы. Переменной e присваивается ссылка на объект Employee, а Employee устанавливается в nil. Тут демонстрирует свою полезность параметр self: будь в методах объекта Employee зашита ссылка на Employee, после выполнения операций они перестали бы работать (ведь переменная Employee будет содержать nil). Параметр self позволяет использовать методы объектов, не заботясь об имени переменной, которой присвоена ссылка на объект.
Ну, а как создавать экземпляры объекта Employee? Для этого задействуем мета-таблицы. Мета-таблицами в Lua именуются таблицы, описывающие правила обращения с некоторым значением – в том числе с другими таблицами. Вот как может выглядеть мета-таблица для объектов Employee:
function Employee:new (name, age, salary, position) obj = {name = name, age = age, salary = salary, position = position} setmetatable(obj, self) self.__index = self return obj end
В результате можно будет написать:
e1 = Employee:new('Vasya Pupkin', 25, 1000, 'manager') e2 = Employee:new('Ivan Petrov', 31, 1500, 'accountant')
и убедиться, что вызовы e1:print() и e2:print() выдают информацию о двух разных сотрудниках.
Я понимаю, что от синтаксических выкрутасов Lua вы уже готовы лезть на стену. Но, как говорят католики из Рио-де-Жанейро, «Терпение и труд все перетрут». Сейчас мы все поймем.
Метамагия
Прежде всего, new – это обычный элемент-функция таблицы Employee. В ней создается новая таблица obj с четырьмя элементами, значения которых берутся из параметров функции. Строка
setmetatable(obj, self)
провозглашает, что Employee – мета-таблица для таблицы obj. Теперь при выполнении над obj нестандартных операций (например, индексации несуществующих элементов) таблица obj будет неявно обращаться к мета-таблице Employee за описанием необходимых действий. Еще интереснее строка
self.__index = self
Она означает, что если при работе с obj произойдет обращение к элементу, отсутствующему в таблице, Lua будет искать элемент с соответствующим ключом в мета-таблице. Заметьте, что, создавая таблицу obj, мы не указывали методов, а значит, вызов
e1:print()
обратится к элементу Employee.print. Благодаря параметру self метод Employee.print будет работать с данными объекта e1, а не Employee. Кстати, теперь присвоение переменной Employee значения nil аннулирует методы всех объектов, созданных с помощью Employee:new(): ведь их описания исчезнут вместе с мета-таблицей. Как вы уже поняли, при работе с объектами мета-таблица играет роль класса. Стало быть, в Lua можно удалить не только данные объекта, но и код его методов (но вашему преподавателю по C++ об этом молчок).
А можем ли мы написать такое?
function Employee:new (name, age, salary, position) obj = {name = name, age = age, salary = salary, position = position, print = self.print} setmetatable(obj, self) self.__index = self return obj end
Да, можем, и тогда при вызове метода print() объекту obj не придется обращаться к мета-таблице. Но наш код потеряет гибкость. Если в ходе выполнения программы описание метода print() в мета-таблице изменится, ранее созданные объекты об этом не узнают: ведь у них уже есть свое поле print, и обращаться к мета-таблице им незачем. Можно, наоборот, полностью перенести описание объекта (не только методов, но и полей) в мета-таблицу. Для этого перепишем функцию Employee:new() так:
function Employee:new (obj) obj = obj or {} setmetatable(obj, self) self.__index = self return obj end
Тогда синтаксис вызова функции Employee:new() тоже изменится:
e1 = Employee:new{name = 'Vasya Pupkin', age = 25, salary = 1000, position = 'manager'}
Обратите внимание на скобки. Этот вариант кажется неудобным: по сути, объект obj конструируется «вручную» и приходится явно указывать имена полей, уже определенных в мета-таблице. Зато легко организовать наследование классов. Пусть нужно создать объект-потомок класса Employee с переопределенным методом print(). Вот что для этого требуется:
function newPrint(self) print('name: '..self.name..' age: '..self.age..' salary: '..self.salary..'position: '..self.position) end e3 = Employee:new{name = 'Ivan Sidorov', age = 20, salary = 800, position='security manager', print = newPrint} e3:print()
Создавая объект e3, мы заменяем функцию Employee:print() на newPrint(). В результате при вызове e3:print() на самом деле будет вызвана функция newPrint() – одним махом мы получаем не только наследование, но и, в некотором смысле, полиморфизм. Тем же способом можно добавлять в объекты-потомки Employee новые поля данных и методы, не меняя описания мета-таблицы Employee.
Мы подошли к важной мысли: скрипты Lua способны само-модифицироваться, а значит, быть самообучаемыми! Я не упоминал об этом достоинстве Lua – не буду врать, что просто забыл, скажу честно: новичкам, не прошедшим вторую стадию посвящения, знать о таком было рано. А что же дальше-то будет?! LXF