Еще один «летний» язык программирования. Часть 2

24 июнь, 2005 - 23:00Андрей Зубинский

Итак, первый шаг в Lua уже сделан. Осталось, в принципе, совсем немного. По некоторому размышлению для этого оставшегося немногого автором выбран пусть вязкий, но совершенно «летний» стиль. Мы будем неспешно и обстоятельно разбирать нюансы Lua – так, чтобы ваша коллекция статей этого цикла могла стать учебником по Lua для тех, кто изучает свой первый скриптовый язык. Однако это ни в коей мере не означает, что статьи будут «обо всем» – охватить все сопутствующие изучению языка темы практически невозможно.

Как и в первой статье этого цикла, мы начнем с отступлений, которые никакого отношения к языку Lua не имеют. Итак, несколько не очень свежих новостей, по тем или иным причинам не привлекших внимания.

Пару лет назад управление образованием штата Вирджиния запросило у компании Texas Instruments (TI) проведение модификации распространенных в школах калькуляторов семейства TI-30 Xa с целью сокращения их функциональности. А именно, для того чтобы модификации калькуляторов этого семейства были пригодны к использованию учениками младших классов, TI должна была убрать функцию преобразования числа в дробь и обратную ей. Что, собственно, TI и сделала. И в течение двух лет больше десяти тысяч калькуляторов благополучно использовались школьниками штата Вирджиния – до тех пор, пока дотошный двенадцатилетний шестиклассник Дакота Браун не обнаружил, что до якобы устраненной функциональности можно добраться одновременным нажатием двух кнопок. В результате этого открытия учащиеся штата Вирджиния едва не избежали обязательного изучения оперирования дробями с помощью карандаша и бумаги, а TI отозвала из школ штата больше 11 тыс. калькуляторов и заменила их моделями теперь уже с гарантированно отсутствующими запрещенными операциями. В перспективе компанию ожидает еще около 160 тыс. подлежащих замене «взломанных» калькуляторов.

Примерно в то же самое время, когда достижения юного хакера Дакоты Брауна стали достоянием соучеников, его более умудренный коллега Гэри МакКиннон во второй раз попался, и теперь как будто всерьез. 39-летнего МакКиннона обвиняют в получении незаконного доступа в течение почти года к полусотне компьютеров Министерства обороны США и NASA. Стоимость «отлова» и устранения последствий нелегальной деятельности МакКиннона превысила миллион долларов.

В двух хакерских историях как-то неуловимо присутствует одна географическая особенность – штат Вирджиния. В деле МакКиннона один из судов этого штата сыграл свою и по-своему замечательную роль, схожую с ролью инженеров TI, укрывших от якобы неразумных младших вирджинских школьников запретную функциональность фиговым листочком из двух кнопок. Суд восточного округа штата Вирджиния распространил pdf-файл с описанием дела «США против Гэри МакКиннона» (на момент написания статьи он был свободно доступен по следующему адресу: news.findlaw.com/hdocs/docs/cyberlaw/usmck1102vaind.pdf). В этом документе такая «незначительная» информация, как IP-адреса взломанных МакКинноном компьютеров (вспомним, каким серьезным организациям принадлежат эти машины), настолько надежно защищена, что для ее получения достаточно штатного бесплатного Adobe Acrobat Reader. Потому что прекрасно читаемые IP-адреса в нем закрыты фиговыми листочками, исполненными в манере художника Малевича, – черными прямоугольниками. И достаточно выделить и скопировать текст под этим прямоугольником, чтобы узнать такую «мелочь», как IP-адрес военного компьютера в Форт-Майерсе или где-нибудь еще.

Какие выводы можно сделать из этих двух вполне очевидных историй «про хакеров»? Выводов можно сделать много. Поэтому автор традиционно делать этого не будет. Но об очевидном умолчать тоже невозможно. А очевидно здесь, по мнению автора, следующее: сколь бы ни проста была, сколь бы ни сложна была человеко-машинная система, она совершенно не может быть совершенной. Такая простая (относительно, конечно) техническая система, как калькулятор, в обрамлении таких непростых людей, как конструкторы Texas Instruments и двенадцатилетний Дакота Браун, так же далека от совершенства, как и такая сложная система, которая образована судебной и военной машинами сверхдержавы. Посему давайте перестанем воспринимать всерьез заявления о «неприступности» и прочих исключительных качествах чего бы то ни было и продолжим наш экскурс в Lua. В конце концов, мы же не верим не менее убедительным обещаниям новых ощущений и даже счастья, которыми безжалостно потчует нас богатое воображение создателей рекламных роликов.

Lua – значения, типы, выражения

Еще один «летний» язык программирования. Часть 2
Рис. 2. Любители мощных средств разработки совершенно необязательно должны ограничивать себя аскезой консоли. Существуют развитые интегрированные среды разработки (IDE), ориентированные на Lua-программистов. Вот одна из них – LuaIDE. Она легально бесплатна и доступна как в бинарном виде, так и в исходных кодах (URL разработки: www.gorlice.net.pl/~rybak/luaide/)

Вторую нашу беседу мы посвящаем поискам ответов на два важнейших вопроса, непременно возникающих у изучающего новый язык программирования. А именно, чем мы можем оперировать с помощью изучаемого языка и как мы можем этим чем-то оперировать.

Первая важнейшая особенность Lua заключается в том, что переменные в нем обладают «контейнерным» свойством. Иначе говоря, тип хранимого в переменной значения является свойством собственно значения, а не переменной. Прежде чем мы проверим это утверждение на примере, давайте ознакомимся с одной специфичной базовой функцией Lua – type(). Данная функция, в качестве аргумента принимающая имя переменной-контейнера, убеждается в существовании контейнера, «заглядывает» в него, определяет тип хранящегося в контейнере значения и возвращает строку, описывающую имя этого типа. Если переданное в качестве аргумента имя не принадлежит какому-либо существующему контейнеру, type() возвращает строку "nil" – единственное уникальное значение в Lua, гарантированно отличающееся от всех возможных значений любого типа. Итак, начнем эксперименты. Запустим Lua в интерактивном режиме и проверим значение гарантированно несуществующего контейнера с именем, например unk (он гарантированно не существует, потому что мы его еще не создавали):

print( type(unk) )

С функцией print() мы уже знакомились в первой части статьи. Функция type() возвращает строковое значение описания типа контейнера unk, которое и выводится «на печать» функцией print(). Результатом выполнения этого примера является упомянутое ранее уникальное значение nil.

Теперь давайте немного изменим этот пример – введем дополнительную переменную-контейнер, например с именем "a":

a = type(unk)

Этим действием мы создали контейнер с именем "a" и положили в него (с помощью операции присваивания) значение типа «строка» – "nil". Проверим данное утверждение. Введем строку print (a) и нажмем Enter – интерпретатор выдаст содержимое переменной-контейнера "a" – "nil". Проверим тип содержимого "a" командой print( type(a) ). Результат – "string", значение типа «строка».

Теперь мы можем уточнить одно важное свойство переменных-контейнеров Lua, а именно, продолжительность их жизни в программе. Lua – язык с автоматической «сборкой мусора». Это означает, что программисту не надо следить за процедурами выделения/освобождения памяти. Но если возникает необходимость принудительно удалить уже существующий переменную-контейнер, можно воспользоваться следующим приемом – поместить в него уникальное значение типа nil, которое в языке представляется так же, как и имя его типа, – просто nil. Прежде создали переменную с именем "a", содержащую значение типа строка. Попробуем удалить этот контейнер следующим действием (введите всю следующую строку и нажмите Enter):

a=nil ; print( type(a) ) ;

Что мы этой строкой приказали выполнить интерпретатору Lua? Сначала записали в контейнер с именем "a" единственно возможное значение типа nil, которое в силу уникальности имеет в языке специальное имя nil, после чего проверили тип значения, хранящегося в контейнере "a", – и он совпал с типом несуществующего контейнера, как в самом первом примере (результатом выполнения этой строки, как вы видите, является строка "nil").

Еще один «летний» язык программирования. Часть 2
Рис. 3. Еще одна мощная IDE, создаваемая компанией Qutix Software в качестве редактора скриптов для игры Pompolic Madness – QDE. Программа распространяется свободно и ни в коей мере не ограничивается потребностями игроков. QDE можно скачать здесь: www.quotixsoftware.com/qde_index.htm

Не откладывая в долгий ящик, сразу определимся, что же происходит на самом деле с контейнером с именем "a" – а точнее, попробуем ответить на вопрос: «Когда именно сборщик мусора Lua физически освободит память, выделенную для контейнера с именем "a"?». Для любых сред выполнения со сборщиками мусора ответ на этот вопрос непрост. Lua не является исключением, но все-таки здесь все немного понятнее: среда исполнения Lua управляет поведением сборщика мусора на основе значений двух скрытых переменных – общей емкости памяти, доступной виртуальной машине Lua (в байтах), и порогового значения этой памяти, при котором срабатывает механизм сборки мусора. В принципе, данное пороговое значение можно установить равным... нулю – при этом сборщик мусора будет срабатывать немедленно. Только в таком случае (когда порог срабатывания сборщика мусора Lua равен нулю) можно говорить о том, что сразу после помещения в контейнер с именем "a" значения nil память, выделенная под данный контейнер, будет физически освобождена. В любых других случаях физическое освобождение памяти непременно наступит, но позже. Когда именно это случится, в принципе, программисту неважно (а не в принципе... есть моменты, при которых пренебрежение особенностями сборки мусора приводит к колоссальным потерям производительности программы, на ряде из них мы остановимся подробнее в последующих частях статьи).

Из последнего однострочного примера можно «выудить» еще одно сведение о Lua – об использовании точки с запятой. В Lua этот символ можно применять для разделения операторов, при записи их в одну строку. Во всех остальных случаях использование ";" необязательно (в том числе и в приведенном примере – последний символ строки в принципе не нужен).

Теперь давайте попробуем окинуть взглядом все типы значений, используемые в Lua. Для значения каждого типа мы предусмотрим ряд исполняемых примеров, чтобы сразу, при чтении статьи, их можно было проверить.

Итак, попробуйте в интерактивном режиме выполнить каждую из этих строк:

print( type("String double-quotes styled") )
print( type('String single-quotes styled') )
print( type([[ Universal string ]]) )
print( type( type( XXX ) ) )

Как вы видите, все они приводят к одному и тому же результату: тип всех значений в этих выражениях – string, или строка. Применение в описаниях использованных значений строкового типа двойных или одинарных кавычек – это вопрос стиля, и чтобы не обижать приверженцев разных стилей, создателями языка было принято решение поддержать оба варианта. У строковых значений, определенных двойными и одинарными кавычками, есть общее свойство – они не могут содержать специальные и непечатные символы, а только обычные символы и так называемые искейп-последовательности (escape sequences), синтаксис и семантика которых заимствована из языка C. Полный их перечень приводить нет смысла (в конце концов, это всего лишь статья, а не полноценное руководство по программированию), достаточно сказать, что синтаксически искейп-последовательности Lua описываются в обобщенном виде "символ", а семантически интерпретируются как вставка в строковое значение специальных или непечатных символов. Например, чтобы использовать в строковом значении, определенном с помощью двойных кавычек, символ двойной кавычки, его придется специфицировать в теле строки с помощью искейп-последовательности """. В противном случае интерпретатор просто не сможет распознать такую «неправильную» строку:

"это "неправильная" строка Lua"
"а это \"правильная\" строка Lua"
'и это "правильная" строка Lua'
"и это 'правильная' строка Lua"
'a это 'неправильная' строка Lua'
'и еще одна \'правильная\' строка Lua'

Строковые значения, описанные с помощью двойных квадратных скобок, напротив, исключают все ограничения. В этом случае в строке правильно интерпретируются все непечатные (в том числе и перевод строки) и специальные символы, а искейп-последовательности теряют свои магические свойства и остаются просто набором символов. Попробуйте исполнить такой пример, чтобы убедиться в этом:

print([[A 'very» special \»Lua\' string ]])

Значения типа строка в Lua, как и прочие, – динамического размера, а механизмы управления памятью столь эффективны, что строки в несколько десятков мегабайт – не редкость в реальных Lua-программах. И наконец, самое важное о строках Lua. Естественно, о разрядности символа в значениях типа строка. В Lua эта разрядность равна 8 битам – чистым восьми битам, без всяких ограничений (которые по сей день не редкость в мире Unix-программ). Но! О чем следует всегда помнить – это все-таки не Unicode! И еще раз – но! Хоть непосредственной поддержки Unicode-строк в Lua нет (а нет ее только потому, что Unicode – огромный и крайне сложный стандарт, и ставить в зависимость от него маленький Lua просто нереально), никто и ничто не запрещает «загонять» в строки Lua, например, последовательности символов в кодировке UTF-8 – «чистая восьмибитность» символов Lua означает, что символ конца строки (байт с нулевым значением) не является в Lua зарезервированным. Что же касается других Unicode-кодировок (UTF-16, UTF-32) и операций над Unicode-строками – мы поговорим об этом позже.

Разговор о значениях типа строка мы завершим кратким обсуждением последней строки примера. В ней вместо XXX можно подставлять что угодно – результатом всегда будет тип строка. Здесь все очевидно – раз функция type() всегда возвращает строковое значение, то его типом ( type(type ()) ) всегда будет string, что и демонстрируется исполнением примера для любого XXX (в том числе и для имен, еще не определенных в системе).

Теперь исследуем реакцию системы на такие команды:

print( type(12) )
print( type(1.275e-20) )
print( type(6.4e7) )
print( type(2.12e+11) )

Все они в результате выполнения выдают один и тот же результат – строку "number". Это свидетельствует о том, что Lua воспринимает все указанные значения как числа. Но не только. Заметим, что реакция на видимо целочисленное значение 12 и, например, на явно не целое 1.275e-20 – одинаковая. То есть для Lua это именно числа, без всяких отдельных, привычных по другим языкам программирования, уточнений (целое и с плавающей точкой). Все дело в том, что в Lua... просто нет значений типа «целое число». Все числа в Lua по умолчанию представляются числами с плавающей точкой двойной точности. Интерпретатор Lua можно собрать из исходных текстов так, чтобы изменить это представление, но только в части точности (например, на числа с плавающей точкой одинарной точности). Неиспользование же целочисленной арифметики является фундаментальным свойством Lua. И надо сказать, что язык от этого ни в чем не проигрывает, а его реализация на современных машинах демонстрирует одни из лучших показателей производительности среди интерпретируемых языков. Что же касается опасений различных нюансов, связанных с «эмуляцией» плавающей точкой целочисленной арифметики... в общем, можете смело использовать математику Lua как целочисленную во всем диапазоне значений длинного целого (long integer) числа 32-битной платформы – никаких проблем это не вызовет.

Итак, мы ознакомились с тремя фундаментальными типами значений Lua – уникальным типом nil, множество значений которого представлено всего одним уникальным, гарантированно отличающимся от всех прочих значений элементом, для которого зарезервировано имя nil, строковыми и числовыми значениями. Прежде чем мы перейдем к изучению оставшихся типов, рассмотрим вкратце операции, которые Lua позволяет производить над значениями указанных типов. Арифметические операции Lua небогаты, но достаточны для несложных вычислений. Сложение, вычитание, умножение, деление, получение числа с обратным знаком (так называемый «унарный минус») – вот и весь их перечень. Синтаксис этих операций совершенно традиционный, так что можете смело попробовать использовать Lua в качестве калькулятора, например:

a=278+476 ; print(a)
a=a-74 ; print(a)
a=a/250 ; print(a)

Знающие языки C и C++ будут немного разочарованы отсутствием в Lua любимых операций типа += (что, кстати, подчеркнуто демонстрируется примером). Кроме того, Lua «как бы поддерживает» арифметическую операцию возведения в степень (бинарный оператор «^»). «Как бы» в данном случае объясняется тем, что реализация этой возможности зависит он наличия в системе, где выполняется интерпретатор Lua, стандартной библиотеки языка C (а точнее, функции pow() из математической библиотеки C). Впрочем, на ваших ПК под управлением Unix-совместимых ОС или Windows следующий пример будет выполняться правильно:

a=2.8
a=a^4 ; print(a)
Еще один «летний» язык программирования. Часть 2
Рис. 1. Карманная версия Lua

«Карманная» версия Lua также с задачей справляется (рис. 1), но все-таки следует помнить – в отличие от базовых операций возведение в степень – платформенно-зависимая возможность арифметики Lua.

Операторы сравнения в Lua внешне вполне традиционны, но здесь язык готовит ряд сюрпризов. Традиционные сравнения «больше – меньше – больше или равно – меньше или равно – равно» в Lua дополнены явной проверкой на неравенство. Сразу продемонстрируем все операторы сравнения на примерах:

a=5 ; b=9 ;
print(a>b)
print(a<b)
print(a<=b)
print(a>=b)
print(a==b)
print(a~=b)

Заметьте, какие значения выводятся в качестве ответов этих примеров. Давайте проверим тип любого из этих значений уже испытанным способом:

print( type(a>b) )

Возвращенное значение boolean является именем четвертого фундаментального типа значений Lua. Множество значений булевого типа, естественно, ограничено только двумя – true и false. Однако булев тип не является обязательным и исключительно допустимым в операциях сравнения и логических операциях Lua (о которых мы еще поговорим). Все эти операции воспринимают булево false и уникальный nil – как «ложно» и любые другие значения – в качестве «истинно».