Lua для игр и не только. Часть 4

О присваивании и не только
Для более легкого дальнейшего чтения перечислим в упрощенном виде некоторые базовые постулаты.
Таблицы объявляются следующим образом:
t={}
Если в фигурных скобках ничего нет, то мы создаем пустую таблицу. Также в фигурных скобках могут перечисляться элементы, которые могут ассоциироваться со значениями, переменными, функциями, другими таблицами, пользовательскими данными, сопрограммами (потоками). Для простоты дальнейшего понимания функции, таблицы, пользовательские данные и сопрограммы мы будем называть объектами. Их отличие от переменных базовых типов (bool, number, string) состоит в том, что они никогда не содержат значений.
Функции в классическом варианте написания объявляются так:
function f(аргументы)
--блок кода
end
В Lua существует два различных типа присваивания: ссылок на значения и ссылок на объекты. Например, мы объявили таблицу и функцию, а теперь вводим переменную N.
. N становится другим именем таблицы t:
N=t
. N получает ссылку на значение из t, если оно определено заранее (иначе nil):
N=t[1]
. N становится другим именем функции f:
N=f
. N получает значение, возвращаемое f:
N=f()
В первом и третьем случаях конкретных значений у N нет, поскольку объекты не имеют значений.
Что интересно
Для того чтобы доказать тот факт, что типы имеют только данные, а не переменные, которые типизируются автоматически, покажем следующий фрагмент кода:
A={23,65}
function B()
return 18
end
A,B=B,A
print(B[1])
Результат: 23. На практике он вам не очень пригодится, но окончательно все прояснит. Итак, изначально A у нас является таблицей, а B — функцией. Строкой A,B=B,A мы поменяли их местами, то есть A уже — функция, а B — таблица. О параллельном присваивании мы подробно расскажем в следующей части, а пока вам достаточно и этих знаний.
Что более интересно
Любые объекты и переменные могут изменяться динамически. Но при этом у многих читателей может появиться легкое недопонимание в вопросе присвоения ссылок на объекты, и почему, например, перечисляя варианты присваивания, мы написали "становится другим именем" функции или таблицы, а не написали "присваивается ссылка на объект". На самом деле это сделано для упрощения описания происходящих процессов.
Пример:
function f(a,b)
return a+b
end
c=f
function f(a,b)
return a-b
end
d=f
print(c(1,2),d(1,2))
Результаты: 3 и -1.
Как вы видите, мы два раза объявили функцию f, в первом случае она вычисляет сумму двух аргументов, а во втором — их разность. В промежутке между двумя объявлениями мы присвоили первой f другое имя, а именно, c. Второй f соответственно было присвоено d. Что нам дают результаты? Обращение c(1,2) фактически направлено на первую f, то есть сумму, а d(1,2) на вторую.
Lua и ООП
По множеству сложившихся стереотипов Lua считается процедурным языком, то есть он не является в общепринятом смысле объектно-ориентированным. Это не так. В данном случае мы воспользуемся тем представлением, что определение ООП достаточно условно, его нельзя взять и вычленить из структурного программирования, поскольку ООП многое из него наследует (первоначально название С++ было "C с классами"). Классы можно представлять неявно на уровне структурированных определенным образом данных и функций. Например, ранее мы, говоря о типе данных table, указали, что в этих таблицах могут располагаться функции, то есть таблицы имеют методы. И вообще объединение объектов по свойствам может производиться по- разному.
Пример кода на Lua:
--объявляем функцию
function f(a,b)
return a+b
end
--создаем таблицу
t1 = {[1]="привет", world="мир",n1=f}
s="world"
--выводим результат
print(t1[1], t1.world, t1[s], t1.n1(5,4))
Результат:
привет мир мир 9
Объяснение. Итак, мы создали функцию f(a, b), которая вычисляет сумму двух чисел, после этого мы сформировали таблицу t1, в качестве элементов которой поместили [1]="привет", world="мир" (аналогично записи ["world"]="мир") и n1=f. То есть первый элемент таблицы можно воспринимать как элемент массива, второй или как элемент массива или как свойство, третий — как метод. Подробнее о конструкторе таблиц мы расскажем чуть позже. Обратите внимание на то, что в рамках Lua стираются некоторые грани, например, написание t1.world эквивалентно t1["world"], а чтобы показать то, что в данной ситуации элемент отобразится как свойство, вместо "мир" напишите true, и это поле (элемент таблицы) автоматически станет булевым.
Пример с кошками…
Lua является объектно-ориентированным языком, только не в том разжеванном смысле, как это есть у других. Инкапсуляция работает в стиле структурного программирования — на уровне функций, объявления локальных переменных. Классы в стандартных языках призваны в основном для реализации решения двух проблем: минимизации кода и оптимизации/автоматизации наследования. Но Lua представляет собой более гибкую структуру. В основном благодаря такому типу данных, как table, который подразумевает и гетерогенные варианты заполнения (то есть эти структуры могут заполняться данными любых типов кроме nil), а также реализованным возможностям присваивания. В коде, представленном выше, вы убедились, что структуры, подобные классам, можно сделать, не используя конструкторы классов, при помощи литералов и создания специальных таблиц. Литерал — это буквальная константа, имя которой одновременно хранит и ее значение.
Рассмотрим следующий фрагмент кода:
cats={main="хищники, млекопитающие",
food="рыба, мясо"}
h_cats={}
h_cats.main=cats.main
h_cats.food=cats.food
--добавляем новое свойство
h_cats.address="квартира"
print(h_cats.food, h_cats.address)
С одной точки зрения, cats — это таблица, с другой — на нее можно смотреть как на класс, в рамках которого мы обозначили свойства: "ключевые" (main) и "пища" (food). Допустим, мы решили создать подкласс домашних кошек h_cats, который наследует все свойства класса cats, но при этом нам необходимо добавить ряд дополнительных. Используем литералы (также можем их перечислить в фигурных скобках).
Ошибки, которые можно допустить, более иллюстративно отображены в следующем коде:
cats={main="хищники, млекопитающие",
food="рыба, мясо"}
h_cats=cats
h_cats.address="квартира"
print(cats.address)
Код работает, но как? Здесь мы сделали присваивание h_cats всей таблицы cats (причем речь идет не о копировании, а об управлении ссылками), а по существу мы говорим об одном и том же объекте, то есть после строки h_cats.address="квартира" такое же свойство добавляется и в cats! Получается, то мы видоизменяем один и тот же объект, но под разными именами.
То есть в предыдущем листинге мы фактически работали с присваиванием значений переменных, а в этом — присвоили объекту второе имя. С этим моментом в Lua нужно быть осторожным.
Как многим уже стало понятно, работа с классами близка к массивам и их представлению, но это не все. Мы говорили о приравнивании объектов, но при этом таблицы по своему определению могут включать объекты любых типов, в том числе и другие таблицы. Пример кода:
cats={main="хищники, млекопитающие",
food="рыба, мясо"}
h_cats={cats=cats, address="квартира"}
print(h_cats.cats.main,cats.food)
В этом листинге мы закрепили за свойством (полем) cats таблицы h_cats ссылку на таблицу cats. В результате, вызывая h_cats.cats.main мы по существу обращаемся к cats.main. Подробнее о создании таблиц мы побеседуем позже, пока мы работаем с классами.
Теперь давайте создадим аналог оператора new, который широко применяется в других языках, здесь мы будем рассматривать его как функцию- конструктор и назовем ее, например, padd.
function padd(fname)
fname.food="сгущенка"
fname.phone="+375296"
--и так далее
end
Все готово. Эта функция-конструктор добавляет свойства-поля food и phone в любую из таблиц. Вызывается просто, например:
h_cats={}
padd(h_cats)
Функции-конструкторы представляют собой очень гибкие структуры, например, внутри их мы можем создавать большие логические и математические взаимосвязи, использовать локальные переменные, производить вычисления, в зависимости от обстоятельств заполнять формы данных (например, в наших листингах с кошками можно добавить функцию по вычислению ежедневного потребления сухого корма в зависимости от указанного веса). То есть получаем максимально открытую архитектуру, но при простом синтаксисе языка Lua.
Как видите, таблицы, в рамках того представления, что нам дает Lua, являются очень мощным инструментом.
Многоликие структуры
Чтобы не загромождать кодом с приведением больших организованных многоликих структур, позволим себе следующий пример. Не смотрите на то, что он не оптимален, задачей стоит просто продемонстрировать возможности применения адаптируемых методов.
function padd(fname)
fname.new_food="сухой корм"
if fname.ves<4 then
function kolvo() return fname.ves*20
end
else
function kolvo() return fname.ves*20+5
end end
fname.kolvo=kolvo
end
cat={ves=3}
padd(cat)
print(cat.new_food)
print(cat.kolvo(), "г в день")
Итак, у нас есть объект (можем воспринимать его как класс) cat, а по существу, кот, в рацион питания которого мы решили добавить сухой корм. Но при этом нам нужно рассчитать количество на день. Если рассмотреть таблицы на пачках с "китикетами", то очевидно, что эта величина рассчитывается как "вес кошки*20 г" для кошек до 4 кг, и "вес кошки*20 г + 5 г" для 4 кг и более.
По аналогии с предыдущим подразделом мы создали функцию добавления свойств, а в данном случае и методов. Мы добавили в рацион питания сухой корм (свойство cat.new_food), а после создали каскад if then else, в котором определили все зависимости и включили формулы. Причем сделали это специальным способом, объявив для каждого из двух случаев (вес < 4 кг, >= 4 кг) функцию kolvo(), которая вычисляет в зависимости от ситуации необходимое значение и возвращает его.
После этого строкой fname.kolvo=kolvo мы присваиваем методу таблицы cats ссылку на одну из выбранных функций kolvo(). Если мы обратимся к функции padd от другого объекта, например, cat2, где указан другой вес, то у него появится и свой метод cat2.kolvo(), который может подразумевать другие варианты вычислений.
Конечно, в рамках рассматриваемого масштаба, код абсолютно не оптимален. Но как быть в ситуациях, когда имеется несколько алгоритмов для обработки какого-либо события, один из них вызывает ошибку и т.п., либо у нас есть множество случаев, в каждом из которых имеется своя специфика.
Практическое программирование отличается от теоретического. Например, многие авторы книг и курсов пытаются показать свою крутость на уровне емких expression-oriented (ориентированных на емкость и выразительность) выражений, но иногда это фактически то же, что учить иностранные языки по сленгу.
В качестве самостоятельного обучения, посмотрите, что выведется в результатах добавления к предыдущему коду этого:
cats={}
for i=1,10,1 do
cats[i]={ves=i}
padd(cats[i])
print("для веса", i, "кг")
print(cats[i].kolvo())
end
Объясните, что, как и почему происходит. Подробнее о специфике задания элементов таблиц, представления их в виде массивов, деревьев и т.п. мы поговорим в следующей части материала.
О чем это мы?
Запутались? Возможно, что немного. А теперь представим реальную ситуацию. В рамках RTS у нас выставлено 12 одинаковых пушек. Они относятся к одному классу, но каждый из экземпляров ведет себя отдельно в зависимости от ситуации, в которой он оказался. То есть, приближается соперник, начинается бой. Так вот, пушки как класс могут быть представлены и на уровне Lua со всеми методами, которые для него предусмотрены. Если пушка оказалась в какой-либо ситуации, для этого экземпляра вызывается соответствующий метод. Методы могут меняться в зависимости от апгрейда технологий. Например, в предыдущем подразделе, если наш кот вырос, то ему нужно по-другому рассчитывать питание. Вызов метода происходит только по мере необходимости, а сами базовые настройки можно делать в рамках Lua-файлов, не касаясь компилируемой части, она — только исполнитель. Таблицы в Lua могут подразумевать не только классы и массивы из стандартных языков программирования, но и деревья из области искусственного интеллекта. Также таблицы прекрасно подходят для программного отображения графов. Возможности языка неисчерпаемы, хотя, как было указано в предыдущей части материала, очень часто его используют на 5-10% от заложенного.
Промежуточное завершение
На самом деле варианты структурного и объектно-ориентированного программирования очень близки по сути. Реализовать классы, их поддержку, при проектировании языка несложно, то есть они бы могли явно присутствовать и в Lua, но большой необходимости в этом нет. Незачем. Разработчики подошли ко всем вопросам с более правильной стороны, сделав акцент на объектах как таковых, их представлении в виде уникальных структур — таблиц. Концепция языка проявляется именно в структуре программирования, и хочется отметить, что Lua — умница в этом плане.
В то же время, очевидно, что некоторые объектно-ориентированные модели в программировании на универсальных языках потеряли гибкость и обладают малой степенью гибкости.
Кристофер christopher@tut.by
Компьютерная газета. Статья была опубликована в номере 19 за 2009 год в рубрике программирование