Программирование на языке...

413

Upload: others

Post on 27-Jun-2020

6 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1
Page 2: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ПрограммированиенаязыкеLua

ТретьеизданиеРобертуИерузалимски

ProgramminginLuaThirdEdition

RobertoIerusalimschy

ПосвящаетсяИде,НоэмииАннеЛучии.

переводиоформление:N1cke

Page 3: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Оглавление

ВведениеАудиторияОтретьемизданииДругиересурсыНекоторыетипографскиесоглашенияЗапускпримеровБлагодарностиI.Язык1.Началоработы1.1.Куски1.2.Некоторыелексическиесоглашения1.3.Глобальныепеременные1.4.АвтономныйинтерпретаторУпражнения2.Типыизначения2.1.Отсутствиезначения(nil)2.2.Логическиезначения(boolean)2.3.Числа(number)2.4.Строки(string)CтроковыелитералыДпинныестрокиПриведениятипов2.5.Таблицы(table)2.6.Функции(function)2.7.Пользовательскиеданные(userdata)2.8.Нити(thread)Упражнения3.Выражения3.1.Арифметическиеоперации

Page 4: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

3.2.Операциисравнения3.3.Логическиеоперации3.4.Конкатенация3.5.Операциядлины3.6.ПриоритетыоперацийКонструкторытаблицУпражнения4.Операторы4.1.Операторыприсваивания4.2.Локальныепеременныеиблоки4.3.УправляющиеконструкцииifthenelsewhilerepeatЧисловойforОбщийfor4.4.break,returnиgotoУпражнения5.Функции5.1.Множественныерезультаты5.2.Вариадическиефункции5.3.ИменованныеаргументыУпражнения6.Ещеразофункциях6.1.Замыкания6.2.Неглобальныефункции6.3.КорректныехвостовыевызовыУпражнения7.Итераторыиобщийfor7.1.Итераторыизамыкания7.2.Семантикаобщегоfor7.3.Итераторыбезсостояния7.4.Итераторысосложнымсостоянием

Page 5: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

7.5.ПодлинныеитераторыУпражнения8.Компиляция,выполнениеиошибки8.1.Компиляция8.2.Предкомпилированныйкод8.3.КодС8.4.Ошибки8.5.Обработкаошибокиисключений8.6.СообщенияобошибкахиобратныетрассировкиУпражнения9.Сопрограммы9.1.Основысопрограмм9.2.Каналыифильтры9.3.Сопрограммыкакитераторы9.4.НевытесняющаямногонитевостьУпражнения10.Законченныепримеры10.1.Задачаовосьмикоролевах10.2.Самыечастовстречающиесяслова10.3.АлгоритмцепиМарковаУпражненияII.Таблицыиобъекты11.Структурыданных11.1.Массивы11.2.Матрицыимногомерныемассивы11.3.Связанныесписки11.4.Очередиидвойныеочереди11.5.Множестваимультимножества11.6.Строковыебуферы11.7.ГрафыУпражнения12.Файлысданнымиисохраняемость12.1.Файлысданными

Page 6: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

12.2.СериализацияСохранениетаблицбезцикловСохранениетаблицсцикламиУпражнения13.Метатаблицыиметаметоды13.1.Арифметическиеметаметоды13.2.Метаметодысравнения13.3.Библиотечныеметаметоды13.4.МетаметодыдоступактаблицеМетаметод__indexМетаметод__newindexТаблицысозначениямипоумолчаниюОтслеживаниедоступактаблицеТаблицы,доступныетолькодлячтенияУпражнения14.Окружение14.1.Глобальныепеременныесдинамическимиименами14.2.Объявленияглобальныхпеременных14.3.Неглобальныеокружения14.4.Использование_ENV14.5._ENVиloadУпражнения15.Модулиипакеты15.1.ФункцияrequireПереименованиемодуляПоискпутиИскатели15.2.ОсновнойподходкнаписаниюмодулейнаLua15.3.Использованиеокружений15.4.ПодмодулиипакетыУпражнения16.Объектно-ориентированноепрограммирование16.1.Классы

Page 7: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

16.2.Наследование16.3.Множественноенаследование16.4.Конфиденциальность16.5.ПодходсединственнымметодомУпражнения17.Слабыетаблицыифинализаторы17.1.Слабыетаблицы17.2.Функциисзапоминанием17.3.Атрибутыобъекта17.4.Вновьтаблицысозначениямипоумолчанию17.5.Эфемерныетаблицы17.6.ФинализаторыУпражненияIII.Стандартныебиблиотеки18.МатематическаябиблиотекаУпражнения19.ПобитоваябиблиотекаУпражнения20.Табличнаябиблиотека20.1.Функцииinsertиremove20.2.Сортировка20.3.КонкатенацияУпражнения21.Строковаябиблиотека21.1.Основныестроковыефункции21.2.ФункциисопоставлениясобразцомФункцияstring.findФункцияstring.matchФункцияstring.gsubФункцияstring.gmatch21.3.Образцы21.4.Захваты21.5.Замены

Page 8: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

КодировкаURLРазложениесимволовтабуляциинапробелы21.6.Специфическиеприемы21.7.ЮникодУпражнения22.Библиотекаввода-вывода22.1.Простаямодельввода-вывода22.2.Полнаямодельввода-выводаНебольшойприемдляувеличениябыстродействияБинарныефайлы22.3.ПрочиеоперациинадфайламиУпражнения23.Библиотекаоперационнойсистемы23.1.Датаивремя23.2.ПрочиесистемныевызовыУпражнения24.Отладочнаябиблиотека24.1.ИнтроспективныесредстваДоступклокальнымпеременнымДоступкнелокальнымпеременнымДоступкдругимсопрограммам24.2.Ловушки24.3.ПрофилированиеУпражненияIV.CAPI25.ОбзорСAPI25.1.Первыйпример25.2.СтекЗаталкиваниеэлементовОбращениекэлементамДругиестековыеоперации25.3.ОбработкаошибоквCAPIОбработкаошибоквприкладномкоде

Page 9: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ОбработкаошибоквбиблиотечномкодеУпражнения26.Расширениевашегоприложения26.1.Основы26.2.Работастаблицами26.3.ВызовыфункцийLua26.4.ОбобщенныйвызовфункцииУпражнения27.ВызываемСизLua27.1.ФункцииС27.2.Продолжения27.3.МодулиСУпражнения28.ПриемынаписанияфункцийС28.1.Обработкамассивов28.2.Обработкастрок28.3.ХранениесостояниявфункцияхСРеестрВерхниезначенияОбщиеверхниезначенияУпражнения29.ЗадаваемыепользователемтипывС29.1.Пользовательскиеданные(userdata)29.2.Метатаблицы29.3.Объектно-ориентированныйдоступ29.4.Доступкаккмассиву29.5.ОблегченныепользовательскиеданныеУпражнения30.Управлениересурсами30.1.Итераторподиректории30.2.ПарсерXMLУпражнения31.Нитиисостояния

Page 10: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

31.1.Многонитевость31.2.СостоянияLuaУпражнения32.Управлениепамятью32.1.Выделяющаяфункция32.2.СборщикмусораAPIсборщикамусораУпражненияПриложения.Lua5.3А.ПереходнаLua5.3А.1.ИзменениявязыкеА.2.ИзменениявбиблиотекахА.3.ИзменениявAPIБ.НовоевLua5.3Б.1.ЯзыкЦелыечислаОфициальнаяподдержка32-битныхчиселПобитовыеоперацииБазоваяподдержкаUTF-8ЦелочисленноеделениеБ.2.БиблиотекиОпцияstripвstring.dumpФункцияtable.moveФункцииstring.pack,string.unpack,string.packsizeБ.3.CAPIОпцияstripвlua.dumpФункцииБ.4.АвтономныйинтерпретаторLua

Page 11: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Введение

КогдаВальдемар,ЛуисияначалиразработкуLuaв1993году,мыструдоммоглисебепредставить,чтоLuaсможеттакраспространиться.Начавшиськакдомашнийязыкдлядвухспецифичныхпроектов,сейчасLua широко используется во всех областях, которые могут получитьвыигрыш от простого, расширяемого, переносимого и эффективногоскриптового языка, таких как встроенные системы, мобильныеустройстваи,конечно,игры.

С самого начала мы разрабатывали Lua для интеграции спрограммным обеспечением, написанным на C/C++ и другихобщепринятыхязыках.Этаинтеграциядаетмногопреимуществ.Lua—этокрошечныйипростойязык,частичноиз-затого,чтооннепытаетсяпревзойти С в том, в чем он уже хорош, например: высочайшеебыстродействие, низкоуровневые операции и взаимодействие состороннимпрограммнымобеспечением.Для этих задачLuaполагаетсянаС.Luaпредлагаеткакразто,длячегоСнедостаточнохорош:высокаястепень независимости от аппаратного обеспечения, динамическиеструктуры, отсутствие избыточности и легкость тестирования иотладки. Для этих целей Lua располагает безопасным окружением,автоматическим управлением памятью и хорошими средствами дляработы со строками и другими видами данных с динамическиизменяемымразмером.

Часть силыLuaидет от его библиотек, и это не случайно.В концеконцов, одной из самых сильных сторон Lua является егорасширяемость. Многие качества языка вносят в это свой вклад.Динамическаятипизацияобеспечиваетвысокуюстепеньполиморфизма.Автоматическое управление памятью упрощает создание интерфейсов,поскольку нет необходимости решать, кто отвечает за выделение иосвобождениепамяти,иликакобрабатыватьеепереполнение.Функциивысшегопорядкаианонимныефункцииобеспечиваютвысокуюстепеньпараметризации,делаяфункцииболееуниверсальными.

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

Page 12: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

C или C++; Lua является «клеем», который мы используем длякомпоновкиисоединенияэтихкомпонентов.Обычно,компоненты(илиобъекты) представляют более конкретные низкоуровневые сущности(такие как виджетыи структурыданных), которыепочтинеменяютсяво время разработки программы, и которые расходуют большуючастьпроцессорного времени итоговой программы. Lua придаетокончательную форму приложению, которая, вероятно, будетнеоднократно изменяться во время жизненного цикла даннойпрограммы. Однако, в отличие от других связующих технологий, Luaприэтомявляетсяполноценнымязыкомпрограммирования.Поэтомумыможем использовать Lua не только для связывания компонентов, но идляихадаптацииинастройки,атакжедлясозданияполностьюновыхкомпонентов.

Разумеется, Lua— не единственный скриптовый язык. Существуютдругие языки, которые вы можете использовать примерно для тех жецелей. Тем не менее, Lua предоставляет набор возможностей, которыеделают его лучшим выбором для многих ваших задач, и которыепридаютемусвойуникальныйпрофиль:

Расширяемость.РасширяемостьLuaнастолькозначительна,чтомногиерассматриваютLuaнекакязык,акакнабордляпостроенияпредметно-ориентированныхязыков(domain-specificlanguage—DSL).МыссамогоначаларазрабатывалиLuaтак,чтобыонбылрасширяемымкакчерезкодLua,такичерезкодС.ВкачестведоказательстваэтойконцепцииLuaреализуетбольшуючастьсвоейбазовойфункциональностичерезвнешниебиблиотеки.ОбеспечитьвзаимодействиеLuaсC/C++действительнопросто,иLuaбылуспешноинтегрировансомногимидругимиязыками,такимикакFortran,Java,Smalltalk,Ada,С#,идажесоскриптовымиязыками,такимикакPerlиPython.Простота.Lua—этопростойималенькийязык.Унегомалоконцепций(затоониэффективные).ЭтапростотаоблегчаетизучениеLuaипомогаетсохранятьегоразмернебольшим.Полныйдистрибутив(исходныйкод,руководство,бинарныефайлыдлянекоторыхплатформ)спокойноразмещаетсянаодномфлоппи-диске.Эффективность.Luaобладаетвесьмаэффективнойреализацией.Независимыетестыпоказывают,чтоLuaявляетсяоднимизсамыхбыстрыхсредискриптовыхязыков.

Page 13: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Переносимость.Когдамыговоримопереносимости,тоимеемввидузапускLuaнавсехплатформах,окоторыхвытолькослышали:всеверсииUnixиWindows,PlayStation,Xbox,MacOSXиiOS,Android,KindleFire,NOOK,Haiku,QUALCOMMBrew,мейнфреймыIBM,RISCOS,SymbianOS,процессорыRabbit,RaspberryPi,Arduinoимногиедругие.Исходныйкоддлякаждойизэтихплатформпрактическиодинаков.Luaнеиспользуетусловнуюкомпиляциюдляадаптациисвоегокодаподразличныемашины;вместоэтогоонпридерживаетсястандартногоANSI(ISO)С.Такимобразом,вамобычноненужноадаптироватьегоподновуюсреду:еслиувасестькомпиляторANSIС,товамвсеголишьнужноскомпилироватьLuaбезкаких-либопредварительныхнастроек.

Аудитория

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

Многие используют Lua, встроенный в какую-либо прикладнуюпрограмму,например,AdobeLightroom,NmapилиWorldofWarcraft.Этиприложения используют Lua-C API для регистрации новых функций,создания новых типов и изменения поведения некоторых операцийязыка, конфигурируя Lua под свою специфическую область. Частопользователи таких приложений даже не знают, что Lua являетсянезависимымязыком, адаптированнымпод даннуюобласть.Например,многие разработчики плагинов для Lightroom не знают о другихспособах использования этого языка; пользователиNmap, как правило,рассматривают Lua как язык Nmap Scripting Engine; игроки вWorld ofWarcraftмогутсчитатьLuaязыкомисключительнодляданнойигры.

Lua также полезен как самостоятельный язык, не только дляобработки текста и одноразовых маленьких программ, но также и дляразличных проектов от среднего до большого размера. При данномприменении основную функциональность Lua дают его библиотеки.Стандартныебиблиотеки,например,предлагаютбазовоесопоставлениес образцом и другие функции для работы со строками. Улучшениеподдержки своих библиотек у Lua привело к быстрому увеличениюколичества внешних пакетов. Lua Rocks, система внедрения иуправлениямодулямидляLua,наданныймоментнасчитываетболее150пакетов.

Page 14: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Наконец, естьпрограммисты, которыеработаютнадругой сторонескамьи, и которые пишут приложения, использующие Lua какбиблиотеку С. Такие люди больше пишут на С, чем на Lua, хотя имтребуется хорошее понимание Lua для создания интерфейсов, которыебудутпростыми,легкимивиспользованииихорошоинтегрированнымисязыком.

Даннойкнигеестьчтопредложитьвсемэтимлюдям.Перваячастьохватывает сам язык, показывая, как мы можем раскрыть весь егопотенциал. Мы фокусируемся на различных конструкциях языка ииспользуем многочисленные примеры и упражнения, чтобы показать,как их использовать для практических задач. Некоторые главы этойчастиохватываютбазовыепонятия,такиекакуправляющиеструктуры,в товремякакостальные главыохватываютболеепродвинутые темы,такиекакитераторыисопрограммы.

Вторая часть полностью посвящена таблицам, единственнойструктуре данных в Lua. Главы этой части обсуждают структурыданных, их сохраняемость, пакеты и объектно-ориентированноепрограммирование.Именнотаммыраскроемнастоящуюмощьязыка.

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

Наконец,последняячастькнигиохватываетAPIмеждуLuaиСдлятех, кто использует C, чтобы овладеть полной мощью Lua. Подачаматериала данной части в силу необходимости весьма отличается отвсей остальной книги. Здесь мы будем программировать на С, а не наLua, так что нам придется сменить амплуа. Для некоторых читателейобсуждение C API может представлять незначительный интерес; длядругихэтачастьможетоказатьсясамойважной.

Отретьемиздании

Эта книга является обновленной и расширенной версией второгоизданиякниги«Programming in Lua» (также известной какPiL 2). Хотяструктуракнигипрактическитажесамая,этоизданиевключаетвсебя

Page 15: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

изрядноеколичествоновогоматериала.Во-первых, я обновил всю книгу до Lua 5.2. Особую значимость

представляет глава об окружениях, которая была практическиполностью переписана. Я также переписал несколько примеров, чтобыпоказать преимущества от использования новых возможностей,предоставляемыхLua 5.2. Тем неменее, я четко обозначил отличия отLua5.1,поэтомувыможетеиспользоватькнигуидляэтойверсииязыка.

Во-вторых, что более важно, я добавил упражнения во все главыкниги. Эти упражнения варьируются от простых вопросов о языке донебольшихполноценныхпроектов.НекоторыепримерыиллюстрируютважныеаспектыпрограммированиянаLuaитакжеважны,какпримеры,которыерасширяютвашнаборполезныхприемов.

Какивслучаеспервымивторымизданиями«ProgramminginLua»,мы опубликовали это третье издание самостоятельно. Несмотря наограниченные возможности распространения, этот подход обладаетрядом преимуществ: мы сохраняем полный контроль над содержимымкниги;мысохраняемвсеправанапредложениекнигивдругихформах;мысвободнывыбирать,когдавыпуститьследующееиздание;мыможембытьуверены,чтовыпускданнойкнигинебудетпрекращен.

Другиересурсы

Справочник (reference manual) по языку необходим всем, ктодействительнохочетегоосвоить.ЭтакниганезаменяетсправочникLua.Напротив,онидополняютдругдруга.СправочниклишьописываетLua.Оннепоказываетнипримеров,ниобъясненийдляконструкцийязыка.Сдругой стороны, он полностью описывает язык: эта книга опускаетнекоторые редко используемые «темные углы» Lua. Более того,справочникявляетсяофициальнымдокументомоLua.Там,гдеэтакнигарасходится со справочником, доверяйте справочнику. Чтобы получитьсправочник и дополнительную информацию о Lua, посетите веб-сайтhttp://www.lua.org.

Вы также можете найти полезную информацию на сайтепользователей Lua, поддерживаемом их сообществом: http://lua-

users.org. Среди прочих ресурсов он предлагает учебное пособие(tutorial), список сторонних пакетов и документации, и архивофициальнойрассылкипоLua.

Эта книга описывает Lua 5.2, хотя большая часть ее содержимоготакже применима к Lua 5.1 и Lua 5.0. Некоторые отличия Lua 5.2 от

Page 16: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

предыдущих версий Lua 5 четко обозначены в тексте книги. Если выиспользуете более новую версию (выпущенную после этой книги),обратитеськсоответственномуруководствупоповодуотличиймеждуверсиями. Если вы используете версию старше 5.2, то пришла пораподуматьобобновлении.

Некоторыетипографскиесоглашения

Вданнойкниге"строковыелитералы"заключенывдвойныекавычки,аодиночные символы, например 'а', заключены в одинарные кавычки.Строки, которые применяются как образцы, также заключены водинарныекавычки,например'[%w_]*'.Книгаиспользуетмоноширинныйшрифт как для кусков кода, так и для идентификаторов. Длязарезервированных слов используется полужирный шрифт. Крупныекускикодапоказанысприменениемследующегостиля:

--программа"HelloWorld"

print("HelloWorld")-->HelloWorld

Обозначение --> показывает результат выполнения оператора или,изредка,результатвыражения:

print(10)-->10

13+3-->16

Посколькудвойнойдефис(--)начинаеткомментарийвLua,выможетебез проблем применять данные обозначения в ваших программах.Наконец,вкнигеиспользуетсяобозначение<-->дляуказаниянато,чточто-тоэквивалентночему-тодругому:

this<-->that

(Примечание: так выглядят примечания от автора книги.) (А таквыглядятпримечанияотпереводчика.)

Запускпримеров

Вам понадобится интерпретатор Lua для запуска примеров из этойкниги. В идеале вам следует использовать Lua 5.2, но большинствопримеровбезкаких-либоизмененийбудетработатьинаLua5.1.

Сайт Lua (http://www.lua.org) хранит исходный код этогоинтерпретатора. Если у вас есть компилятор С и знание того, как

Page 17: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

скомпилироватькодСнавашемкомпьютере,товамстоитпопробоватьустановить Lua из его исходного кода; это действительно легко. СайтLua Binaries (поищите luabinaries) предлагает заранеескомпилированные интерпретаторы для большинства основныхплатформ. Если вы используете Linux или другую UNIX-подобнуюсистему, вы можете проверить репозиторий вашего дистрибутива;некоторые дистрибутивы уже предлагают готовые пакеты с Lua. ДляWindows хорошим выбором является Lua for Windows (поищитеluaforwindows),являющийся«заряженнойсредой»дляLua.Онвключаетвсебяинтерпретатор,интегрированныйтекстовыйредакторимножествобиблиотек.

ЕсливыиспользуетеLua,встроенныйвприложение,такоекакWoWили Nmap, то вам может понадобиться руководство по данномуприложению (или помощь «местного гуру»), чтобы разобраться, какзапускать ваши программы. Тем не менее, Lua остается все тем жеязыком; большинство вещей, которое мы увидим в этой книге,применимо независимо то того, как вы используете Lua. Однако, ярекомендую начать изучение Lua с автономного интерпретатора длязапускавашихпервыхпримеровиэкспериментов.

Благодарности

Прошло уже почти десять лет с тех пор, как я опубликовал первоеизданиеэтойкниги.Ряддрузейиорганизацийпомогалмненаэтомпути.

Каквсегда,ЛуигХенрикдеФигуредоиВальдемарСелес,соавторыLua,предложиливсевидыпомощи.АндреКарригал,АскоКауппи,БреттКапилик, ДиегоМехаб, ЭдвинМорагас, Фернандо Джефферсон, ГэвинВрес, Джон Д. Рамсделл и Норман Ремси предоставили неоценимыезамечания и полезную аналитическую информацию для различныхизданийэтойкниги.

Луиза Новаэс, глава отдела искусства и дизайна в PUC-Rio, смогланайти время в своем занятом графике, чтобы создать идеальнуюобложкудляданногоиздания.

LightningSource,Inc.предложилонадежныйиэффективныйвариантдляпечатиираспространенияданнойкниги.Безнихосамостоятельнойпубликацииэтойкнигинемоглобыбытьиречи.

Центр латиноамериканских исследований в Стендфордскомуниверситете предоставил мне крайне необходимый перерыв отрегулярной работы в очень стимулирующем окружении, во время

Page 18: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

которогояисделалбольшуючастьработынадтретьимизданием.ЯтакжехотелбыпоблагодаритьПапскийкатолическийуниверситет

Рио де Жанейро (PUC-Rio) и Бразильский национальныйисследовательский совет (CNPq) за их постоянную поддержку моейработы.

Наконец, я должен выразить мою глубокую благодарность НоэмиРодригес за все виды помощи (в том числе и технической) искрашиваниемоейжизни.

Page 19: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ЧастьI

Язык

Page 20: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА1

Началоработы

Продолжаясложившуюсятрадицию,нашаперваяпрограмманаLuaвсеголишьпечатает"HelloWorld":

print("HelloWorld")

Если вы используете автономный интерпретатор Lua, то все, что вамтребуется для запуска вашей первой программы — это вызватьинтерпретатор (обычно он называется lua или lua5.2) с именемтекстового файла, содержащего вашу программу. Если вы сохранитевышеприведеннуюпрограммувфайлhello.lua, тоследующаякомандадолжнаегозапустить:

%luahello.lua

В качестве более сложного примера следующая программаопределяет функцию для вычисления факториала заданного числа,запрашиваетупользователячислоипечатаетегофакториал:

--определяетфункциюфакториала

functionfact(n)

ifn==0then

return1

else

returnn*fact(n-1)

end

end

print("enteranumber:")

a=io.read("*n")--считываетчисло

print(fact(a))

1.1.Куски

Каждый выполняемый Lua фрагмент кода, такой как файл илиотдельная строка в интерактивном режиме, называется куском.Кусок (chunk) — это просто последовательность команд (илиоператоров).

Luaненуженразделительмеждуидущимиподряд операторами, но

Page 21: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

вы можете использовать точку с запятой, если хотите. Мое личноеправило— использовать точки с запятой только для разделения двухили более операторов, записанных в одной строке. Переводы строк неиграют никакой роли в синтаксисе Lua; например, следующие четырекускадопустимыиэквивалентны:

a=1

b=a*2

a=1;

b=a*2;

a=1;b=a*2

a=1b=a*2--уродливо,нодопустимо

Кусок может быть просто одиночным оператором, как в примере«Hello World», или состоять из набора операторов и определенийфункций (которые на самом деле являются присваиваниями, как мыувидим позже), как в примере с факториалом. Кусок может бытьнастолько большим, насколько вы захотите. Поскольку Lua такжеиспользуетсякакязыкдляописанияданных,кускивнесколькомегабайтне являются редкостью. У интерпретатора Lua не возникает никакихпроблемприработесбольшимикусками.

Вместо записи вашей программы в файл вы можете запуститьавтономныйинтерпретаторвинтерактивномрежиме.Есливызапуститеluaбезаргументов,тоувидитеегоприглашениеввода:

%lua

Lua5.2Copyright(С)1994-2012Lua.org,PUC-Rio

>

С этого момента каждая команда, которую вы наберете (как,например, print "Hello World"), выполняется сразу после ее ввода. Длявыхода из интерактивного режима и интерпретатора просто наберитеуправляющийсимволконцафайла(ctrl-DвUNIX,ctrl-ZвWindows)иливызовите функцию exit из библиотеки операционной системы — дляэтогонужнонабратьos.exit().

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

Page 22: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

наподобие функции factorial прямо в интерактивном режиме. Тем неменее,обычноболееудобнопомещатьподобныеопределениявфайлизатемвызыватьLuaдляеговыполнения.

Выможетеиспользоватьопцию-i,чтобыдатьуказаниеLuaначатьинтерактивныйсеанспослевыполнениязаданногокуска:

%lua-iprog

Строка с командой вроде этой выполнит кусок в файле prog и затемвыведет приглашение ввода интерактивного режима. Это особенноудобнодляотладкиитестированиявручную.Вконцеданнойглавымырассмотримдругиеопцииавтономногоинтерпретатора.

Другой способ выполнять куски состоит в применении функцииdofile,котораянемедленновыполняетфайл.Например,допустим,увасестьфайлlib1.luaсоследующимкодом:

functionnorm(x,y)

return(x^2+y^2)^0.5

end

functiontwice(x)

return2*x

end

Тогдавинтерактивномрежимевыможетенабрать>dofile("lib1.lua")--загружаетвашубиблиотеку

>n=norm(3.4,1.0)

>print(twice(n))-->7.0880180586677

Функция dofile также удобна, когда вы тестируете фрагмент кода.Выможетеработатьсдвумяокнами:однобудеттекстовымредакторомс вашей программой (скажем, в файле prog.lua), а другое консолью,выполняющей Lua в интерактивном режиме. После сохраненияизменений в вашей программе, вы выполняете dofile("prog.lua") вконсолиLuaдлязагрузкиновогокода;затемвыпроверяетеэтотновыйкод,вызываяегофункцииипечатаярезультаты.

1.2.Некоторыелексическиесоглашения

Идентификаторы (или имена) в Lua могут быть любойпоследовательностью из букв, цифр и символов подчеркивания, неначинающейсясцифры,например:

Page 23: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

iji10_ij

aSomewhatLongName_INPUT

Вы должны избегать идентификаторов, начинающихся с символаподчеркивания,закоторымследуетоднаилинесколькозаглавныхбукв(как,например,_VERSION);онизарезервированывLuaдляособыхцелей.Обычно я использую идентификатор _ (одиночный символподчеркивания)дляпустыхпеременных.

ВстарыхверсияхLuaпонятиетого,чтоявляетсябуквой,зависелоотлокали.Однако,подобныебуквыделаютвашупрограммунепригоднойдлявыполнениянасистемах,которыенеподдерживаютданнуюлокаль.Поэтому Lua 5.2 разрешает использовать в идентификаторах толькобуквыиздиапазоновA-Zиa-z.

Следующиесловазарезервированы;мынеможемиспользоватьихвкачествеидентификаторов:

andbreakdoelseelseif

endfalsegotoforfunction

ifinlocalnilnot

orrepeatreturnthentrue

untilwhile

Luaучитываетрегистрбукв:and—этозарезервированноеслово,ноAndиAND—этодваотличныхотнегоидруготдругаидентификатора.

Комментарий начинается в любом месте с двойного дефиса (--) идлится до конца строки кода. Lua также поддерживает блочныйкомментарий, который начинается с -[[ и длится до ближайших ]].(Примечание: Блочные комментарии могут быть более сложными, какмы увидим в разделе 2.4). Распространенным приемом длязакомментирования фрагмента кода является заключение этого кодамежду--[[и--]],какпоказанониже:

--[[

print(10)--ничегонепроисходит(закомментировано)

--]]

Длявосстановленияработоспособностиэтогокодамыдобавляемодиндефискпервойстроке:

---[[

print(10)-->10

--]]

Впервомпримере--[[впервойстрокеначинаетблочныйкомментарий,

Page 24: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

а двойной дефис в последней строке по-прежнему находится внутриэтого комментария. Во втором примере последовательность ---[[

начинает обычный однострочный комментарий, поэтому первая ипоследняя строки становятся независимыми комментариями. В этомслучаеprintнаходитсявнекомментариев.

1.3.Глобальныепеременные

Глобальным переменным не нужны объявления; вы их простоиспользуете. Обратиться к неинициализированной переменной неявляется ошибкой; вы всего лишь получите значение nil в качестверезультата:

print(b)-->nil

b=10

print(b)-->10

Есливыприсвоитеnil глобальнойпеременной, тоLuaповедетсебятак,какеслибыэтапеременнаяникогданеиспользовалась:

b=nil

print(b)-->nil

После данного присваивания Lua может со временем высвободитьпамять,выделеннуюподэтупеременную.

1.4.Автономныйинтерпретатор

Автономный интерпретатор (также называемый lua.с в связи сназванием его исходного файла или просто lua из-за имени еговыполнимого файла) — это небольшая программа, которая позволяетиспользоватьLua непосредственно.В данном разделе представлены ееосновныеопции.

Когда интерпретатор загружает файл, то он пропускает первуюстроку кода, если она начинается с октоторпа ('#'). Это свойствопозволяет использовать Lua как скриптовый интерпретатор в UNIX-системах.Есливыначнетевашскриптсчего-нибудьвроде

#!/usr/local/bin/lua

(при условии, что автономный интерпретатор находится в/usr/local/bin)или

Page 25: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

#!/usr/bin/envlua

то вы можете вызвать ваш скрипт напрямую, без явного вызоваинтерпретатораLua.

Использованиеluaследующее:lua[options][script[args]]

Всепараметрынеобязательны.Какмыужевидели, когдамы запускаемluaбезаргументов,интерпретаторпереходитвинтерактивныйрежим.

Опция-eпозволяетнамвводитькодпрямовкоманднойстроке,какпоказанониже:

%lua-е"print(math.sin(12))"-->-0.53657291800043

(UNIX требует двойных кавычек, чтобы командная оболочка несталаинтерпретироватькруглыескобки).

Опция -l загружает библиотеку. Как мы уже видели ранее, -iвызывает интерактивный режим после выполнения остальныхаргументов.Такимобразом,следующийвызовзагрузитбиблиотекуlib,затем выполнит присваивание х=10 и выведет приглашение ввода длявзаимодействия.

%lua-i-llib-е"х=10"

В интерактивном режиме вы можете напечатать значение любоговыражения, набрав строку, начинающуюся со знака равенства, закоторымследуетвыражение:

>=math.sin(3)-->0.14112000805987

>а=30

>=а-->30

ЭтоособенностьпозволяетиспользоватьLuaкаккалькулятор.Перед выполнением своих аргументов интерпретатор ищет

переменную окружения с именем LUA_INIT_5_2 или, если такойпеременнойнет,LUA_INIT.Еслиоднаизэтихпеременныхприсутствуетисодержит@имя_файла,тоинтерпретаторвыполнитзаданныйфайл.ЕслиLUA_INIT_5_2(илиLUA_INIT)определена,ноеесодержимоененачинаетсяс '@', тоинтерпретаторсчитает,чтоонасодержиткодLuaивыполняетего. LUA_INIT предоставляет огромные возможности поконфигурированию автономного интерпретатора, поскольку приконфигурированиинамдоступнавсяфункциональностьLua.Мыможемпредварительно загружать пакеты, изменять текущий путь, определять

Page 26: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

нашисобственныефункции,переименовыватьилиуничтожатьфункцииит.д.

Скрипт может получать свои аргументы из предопределеннойглобальной переменной arg. При таком вызове, как %lua script a b c,интерпретаторпередвыполнениемскриптасоздасттаблицуargсовсемиаргументамикоманднойстроки.Имяскриптарасполагаетсяпоиндексу0,первыйаргумент(впримереэтоа)располагаетсяпоиндексу1ит.д.Предшествующие опции располагаются по отрицательным индексам,поскольку они находятся перед скриптом. Например, рассмотрим этотвызов:

%lua-e"sin=math.sin"scriptab

Интерпретаторсобираетаргументыследующимобразом:arg[-3]="lua"

arg[-2]="-e"

arg[-1]="sin=math.sin"

arg[0]="script"

arg[1]="a"

arg[2]="b"

Чаще всего скрипт использует только положительные индексы (впримереэтоarg[1]иarg[2]).

Начиная с Lua 5.1 скрипт также может получить свои аргументыпосредством выражения с переменным числом аргументов. В главномтеле скрипта выражение ... (три точки) передает в скрипт этиаргументы(мыобсудимвыраженияспеременнымчисломаргументоввразделе5.2).

Упражнения

Упражнение 1.1. Запустите пример с факториалом. Что случится свашей программой, если вы введете отрицательное число? Изменитепример,чтобыизбежатьэтойпроблемы.

Упражнение1.2.Запуститепримерtwice,одинраззагружаяфайлприпомощи опции -l, а другой раз через dofile. Что для васпредпочтительнее?

Упражнение1.3.Можетеливыназватьдругойязык,использующий--длякомментариев?

Упражнение1.4.Какиеизследующихстрокявляютсядопустимымиидентификаторами?

Page 27: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

____endEndenduntil?nilNULL

Упражнение 1.5. Напишите простой скрипт, который печатает своеимя,незнаяегозаранее.

Page 28: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА2

Типыизначения

Lua — язык с динамической типизацией. В нем нет определенийтипов;каждоезначениесодержитвсебесвойсобственныйтип.

ВLuaсуществуетвосемьбазовыхтипов:nil,boolean,number, string,table, function, userdata и thread. Функция type возвращает имя типалюбогозаданногозначения:

print(type("Helloworld"))-->string

print(type(10.4*3))-->number

print(type(print))-->function

print(type(type))-->function

print(type(true))-->boolean

print(type(nil))-->nil

print(type(type(X)))-->string

Последняя строка вернет string вне зависимости от значения X,посколькурезультатtypeвсегдаявляетсястрокой.

У переменных нет предопределенных типов; любая переменнаяможетсодержатьзначениялюбоготипа:

print(type(a))-->nil('a'неинициализирована)

a=10

print(type(a))-->number

a="astring!!"

print(type(a))-->string

a=print--да,этодопустимо!

a(type(a))-->function

Обратите внимание на последние две строки: в Lua функцииявляютсязначениямипервогокласса;имиможноманипулировать,какилюбымидругими значениями. (Болееподробномырассмотримданныесредствавглаве6.)

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

2.1.Отсутствиезначения(nil)

Page 29: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

2.2.Логическиезначения(boolean)

Тип boolean обладает двумя значениями — true и false, которыепредставляюттрадиционныелогические(илибулевы)значения.Однако,не только булевы значения могут выражать условие: в Lua условиеможет быть представлено любым значением. Проверки условий(например, условий в управляющих структурах) считают nil и булевоfalse ложными, а все прочие значения истинными. В частности, припроверках условий Lua считает ноль и пустую строку истиннымизначениями.

Напротяженииданнойкниги«ложными»будутназыватьсязначенияnilи false,а«истинными»—всеостальные.Когдаречьидетименнообулевыхзначениях,ониуказываютсяявнокакfalseилиtrue.

2.3.Числа(number)

Тип number представляет вещественные числа, т.е. числа двойнойточности с плавающей точкой (тип double в C). В Lua нетцелочисленноготипа(типintegerвC).

Некоторые опасаются, что даже простые операции инкремента илисравнениямогутнекорректноработатьсчисламисплавающейточкой.Однако,на самомделе этоне так.Практическивсеплатформывнашевремя поддерживают стандарт IEEE 754 для представления чисел сплавающей точкой. Согласно этому стандарту, единственнымвозможным источником ошибок является ошибка представления,которая происходит, когда число не может быть точно представлено.Операция округляет свой результат, только если у него нет точногопредставления. Любая операция, результат которой может быть точнопредставлен,должнавыдаватьегобезокругления.

Ha самом деле любое целое число вплоть до 253 (приблизительно

Page 30: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

1016) имеет точное представление в виде числа двойной точности сплавающей точкой. Когда вы используете вещественные числа дляпредставленияцелых,никакихошибококруглениянепроисходит,покамодуль числа не превышает 253. В частности, число Lua можетпредставлять любые 32-битовые целые числа без проблем сокруглением.

Разумеется,удробныхчиселмогутбытьошибкипредставления.Этаситуациянеотличаетсяоттой,когдавыпользуетесьручкойибумагой.Еслимы хотим записать 1/7 в десятичном виде, то мы должны будемгде-то остановиться. Если мы используем десять цифр дляпредставления числа, то 1/7 будет округлено до 0.142857142. Если мывычислим1/7*7припомощидесятицифр, тополучим0.999999994, чтоотличается от 1. Более того, дробные числа, которые имеют конечноепредставление в десятичном виде, могут иметь бесконечноепредставление в двоичномвиде.Например,12.7-20+7.3 не равнонулю,поскольку у обоих чисел, 12.7 и 7.3, нет точного конечногопредставлениявдвоичномвиде(см.упражнение2.3).

Прежде чем мы продолжим, запомните: у целых чисел есть точноепредставлениеипотомунетошибококругления.

Большинство современных процессоров выполняет операции сплавающей точкой так же быстро, как и с целыми числами (или дажебыстрее). Тем не менее, легко скомпилировать Lua так, чтобы ониспользовал для числовых значений другой тип, например, длинныецелыечисла(типlongвC)иличислаодинарнойточностисплавающейточкой (тип float в C). Это особенно удобно для платформ безаппаратной поддержки чисел с плавающей точкой, например, длявстраиваемых систем. Подробности смотрите в файле luaconf.h издистрибутива.

Мыможемзаписыватьчисловыеконстантывместеснеобязательнойдесятичной дробной частью и необязательным десятичным порядком.Примерыдопустимыхчисловыхконстант:

40.44.57е-30.3е125Е+20

Более того, мы также можем использовать шестнадцатеричныеконстанты,применивпрефикс0x.НачинаясLua5.2,шестнадцатеричныеконстанты также могут иметь дробную часть и двоичный порядок (спрефиксом'р'или'Р'),каквследующихпримерах:

Oxff(255)0х1АЗ(419)0x0.2(0.125)0х1р-1(0.5)

Page 31: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

0ха.Ьр2(42.75)

(Длякаждойконстантымыдобавиливкруглыхскобкахеедесятичноепредставление.)

2.4.Строки(string)

Тип string в Lua имеет обычный смысл: последовательностьсимволов. Lua поддерживает все 8-битовые символы, и его строкимогутсодержатьсимволыслюбымичисловымикодами,включаянуль-символы. Это значит, что вы можете хранить в виде строк любыебинарныеданные.ВытакжеможетехранитьстрокиЮникодавлюбомпредставлении(UTF-8,UTF-16ит.д.).Стандартнаябиблиотека,котораяидетвместесLua,непредлагаетявнуюподдержкуэтихпредставлений.Тем не менее, вы можете работать со строками UTF-8 вполнепривычнымобразом,чтобудетрассмотреновразделе21.7.

Строки в Lua являются неизменяемыми значениями. Вы не можетеизменитьсимволвнутристроки,каквыделаетеэтовС;вместоэтоговысоздаете новую строку с необходимыми изменениями, как показано вследующемпримере:

a="onestring"

b=string.gsub(a,"one","another")--меняетчастистроки

print(a)-->строка"one"

print(b)-->строка"another"

Строки в Lua подвержены автоматическому управлению памятью,какилюбыедругиеобъектыLua(таблицы,функцииит.д.).Этозначит,что вам не нужно беспокоиться о выделении и освобождении строк;этимзавасзайметсяLua.Строкаможетсостоятьизодногосимволаилицелой книги. Программы, которые обрабатывают строки из 100К или10Мсимволов,—нередкостьвLua.

Выможетеполучитьдлинустроки,используяпрефикснуюоперацию'#'(называемуюоперациейдлины):

a="hello"

print(#a)-->5

print(#"good\0bye")-->8

Cтроковыелитералы

Мы можем определять границы строковых литералов при помощи

Page 32: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

пародинарныхилидвойныхкавычек:

a="aline"

b='anotherline'

Этивидызаписиэквиваленты;единственноеотличиесостоитвтом,что внутри одного вида кавычек вы можете использовать другой, неприменяя экраны (т.е. экранированные/управляющиепоследовательности).

Это дело вкуса, но большинство программистов всегда используеткавычкиодноговидадляоднихитехжевидовстрок,где«виды»строкзависятотпрограммы.Например,библиотека,котораяработаетсXML,может зарезервировать строки в одинарных кавычках для фрагментовXML,посколькуэтифрагментычастосодержатдвойныекавычки.

Строки в Lua могут содержать следующие C-образныеэкранированныепоследовательности:\азвонок\bвозвратнаоднупозицию(backspace)\fпереводстраницы\nпереводстроки\гвозвраткаретки\tгоризонтальнаятабуляция\vвертикальнаятабуляция\\обратныйслеш\"двойнаякавычка\'одинарнаякавычкаСледующиепримерыиллюстрируютихиспользование:

>print("oneline\nnextline\n\"inquotes\",'inquotes'")

oneline

nextline

"inquotes",'inquotes'

>print('abackslashinsidequotes:\'\\\'')

abackslashinsidequotes:'\'

>print("asimplerway:'\\'")

asimplerway:'\'

Мытакжеможемзадатьсимволвстрокеприпомощиегочисловогозначения через экранированные последовательности \ddd и \x\hh, гдеddd—этопоследовательностьнеболеечемизтрехдесятичныхцифр,ahh — последовательность ровно из двух шестнадцатеричных цифр. В

Page 33: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

качестве примера посложнее возьмем два литерала: "alo\n123\" и'\971o\10\04923'. Они обладают одним и тем же значением в системе,использующей ASCII: 97 — это код ASCII для 'а', 10 — это код длясимволапереводастроки,а49—этокоддляцифры'1'.(Вэтомпримеремыдолжнызаписатьзначение49припомощитрехдесятичныхцифркак\049, поскольку за ним следует другая цифра; иначе Lua прочтет эточисло как 492). Мы также можем записать эту строку как'\x61\x6c\x6f\x0a\x31\x32\x33\x22', представляя каждый символ егошестнадцатеричнымкодом.

Дпинныестроки

Мыможемзадаватьстроковыелитералыприпомощипариздвойныхквадратных скобок, как мы делали это с длинными комментариями.Литералы в этой скобочной форме могут занимать несколько строк, аэкранированные последовательности в этих строках не будутинтерпретироваться. Более того, эта форма игнорирует первый символстроки, если это перевод строки. Эта форма особенно удобна длянаписания строк, содержащих большие фрагменты кода, как вследующемпримере:

page=[[

<html>

<head>

<title>AnHTMLPage</title>

</head>

<body>

<ahref="http://www.lua.org">Lua</a>

</body>

</html>

]]

write(page)

Иногда вам может потребоваться заключить в квадратные скобкифрагмент кода, который содержит нечто вроде a = b[c[i]] (обратитевнимание на ]] в этом коде) или содержит уже закомментированныйфрагмент. Чтобы справиться с подобными ситуациями, вы можетедобавить любое количество знаков равенства между двумяоткрывающими квадратными скобками, например, так: [===[. Послеэтого изменения строковый литерал завершится только на следующихзакрывающихквадратных скобках с темже самымколичеством знаков

Page 34: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

равенства между ними (]===] для нашего примера). Сканер Luaигнорируетпарыскобоксразнымколичествомзнаковравенства.Путемподбора подходящего количества знаков равенства вы можетезаключить в квадратные скобки любой строковый литерал безнеобходимостидобавлятьвнегоэкраны.

Такая же возможность действует и для комментариев. Например,если вы начнете длинный комментарий с --[=[, то он будетпродолжаться вплоть до следующей пары ]=]. Эта возможностьпозволяетзапростозакомментироватьфрагменткода,которыйсодержитужезакомментированныечасти.

Длинные строки— это идеальныйформат для включения текста ввиде литерала в ваш код, но вам не следует использовать их длянетекстовых литералов. Хотя строковые литералы в Lua могутсодержатьлюбыесимволы,использоватьтакиесимволывсвоемкоде—неоченьхорошаяидея:выможетестолкнутьсяспроблемамиприработес вашим текстовым редактором; более того, последовательности длязавершениястрокнаподобие"\r\n"могутпричтенииизменитьсяна"\n".Вместо этого лучше кодировать произвольные бинарные данные припомощичисловых(десятичныхишестнадцатеричных)экранированныхпоследовательностей, таких как "\х13\х01\хА1\хВВ". Однако, этопредставляетпроблемудлядлинныхстрок,посколькуможетпривестикстрокамдовольнобольшогоразмера.

Для подобных ситуаций Lua 5.2 предлагает экранированнуюпоследовательность \z: она пропускает все последующие символы встрокевплотьдопервогонепробельного символа.Следующийпримериллюстрируетееприменение:

data="\x00\x01\х02\x03\x04\x05\x06\x07\z

\x08\x09\xOMxOB\xOC\xOD\xOE\xOF"

Находящаясявконцепервойстроки\zпропускаетпоследующийконецстроки и отступ следующей строки, поэтому в итоговой строке забайтом\х07сразужеследуетбайт\х08.

Приведениятипов

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

Page 35: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print("10"+1)-->11

print("10+1")-->10+1

print("-5.3e-10"*"2")-->-1.06e-09

print("hello"+1)--ОШИБКА(невозможнопреобразовать"hello")

Lua применяет подобные преобразования не только в арифметическихоперациях, но и в других местах, где ожидается число, таких какаргументmath.sin.

Верноиобратное—каждыйраз, когдаLuaнаходитчисло там, гдеожидаетстроку,онпреобразуетэточисловстроку:

print(10..20)-->1020

(Операция .. служит в Lua для конкатенации строк. Когда вызаписываетееесразупослечисла,выдолжныотделитьихдруготдругапри помощи пробела; иначе Lua решит, что первая точка — этодесятичнаяточкачисла.)

Сегоднямыне уверены, что эти автоматические приведения типовбыли хорошей идеей в дизайне Lua. Как правило, лучше на них нерассчитывать. Они удобны в некоторых местах; но добавляютсложностикакязыку,такипрограммам,которыеихиспользуют.Вконцеконцов, строки и числа — это разные вещи, несмотря на все этипреобразования. Сравнение вроде 10="10" дает в результате false,поскольку10—эточисло,а"10"—этострока.

Если вам нужно явно преобразовать строку в число, то вы можетевоспользоватьсяфункциейtonumber,котораявозвращаетnil,еслистроканеявляетсяправильнымчисломLua:

line=io.read()--читаетстроку

n=tonumber(line)--пытаетсяпреобразоватьеевчисло

ifn==nilthen

error(line.."isnotavalidnumber")

else

print(n*2)

end

Для преобразования числа в строку вы можете использоватьфункциюtostringиликонкатенироватьчислоспустойстрокой:

print(tostring(10)=="10")-->true

print(10..""=="10")-->true

Этипреобразованиявсегдаработают.

2.5.Таблицы(table)

Page 36: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Тип table представляет ассоциативные массивы. Ассоциативныймассив — это массив, который может быть индексирован не толькочислами,ноистрокамиилилюбымдругимзначениемязыка,кромеnil.

Таблицы являются главным (на самом деле единственным)механизмом структурирования данных в Lua, притом оченьэффективным. Мы используем таблицы для представления обычныхмассивов, множеств, записей и других структур данных простым,однороднымиэффективнымспособом.ТакжеLuaиспользуеттаблицыдля представления пакетов и объектов. Когда мы пишем io.read, мыдумаемо«функцииreadизмодуляio».ДляLuaэтовыражениеозначает«индексироватьтаблицуio,используястрокуreadвкачествеключа».

Таблицы в Lua не являются ни значениями, ни переменными; ониобъекты. Если вы знакомы с массивами в Java или Scheme, то выпонимаете, что я имею в виду. Вы можете рассматривать таблицу какдинамически выделяемый объект; ваша программа работает только соссылками (указателями) на них. Lua никогда не прибегает к скрытомукопированию или созданию новых таблиц. Более того, вам не нужнообъявлятьтаблицувLua;насамомделедляэтогодаженесуществуетспособа. Вы создаете таблицы при помощи выражения-конструктора,котороевсвоейпростейшейформезаписываетсякак{}:

a={}--создаеттаблицуисохраняетссылкунанеев'a'

k="x"

a[k]=10--новаязаписьсключом"x"изначением10

a[20]="great"--новаязаписьсключом20изначением"great"

print(a["x"])-->10

k=20

print(a[k])-->"great"

a["x"]=a["x"]+1--инкрементируетзапись"x"

print(a["x"])-->11

Таблица всегда анонимна. Не существует постоянной связи междупеременной,котораяхраниттаблицу,исамойтаблицей:

a={}

a["x"]=10

b=a--'b'ссылаетсянатужетаблицу,чтои'a'

print(b["x"])-->10

b["x"]=20

print(a["x"])-->20

a=nil--лишь'b'по-прежнемуссылаетсянатутаблицу

b=nil--ссылокнатаблицунеосталось

Когда в программе больше не остается ссылок на таблицу, сборщик

Page 37: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

мусора Lua со временем удалит эту таблицу, чтобы повторноиспользоватьеепамять.

Каждаятаблицаможетхранитьзначениясразнымитипамииндексовирастетпомередобавленияновыхзаписей:

a={}--пустаятаблица

--создает1000новыхзаписей

fori=1,1000doa[i]=i*2end

print(a[9])-->18

a["x"]=10

print(a["x"])-->10

print(a["y"])-->nil

Обратите внимание на последнюю строку: как и глобальныепеременные,полятаблицывозвращаютnil,когданеинициализированы.Также,какисглобальнымипеременными,выможетеприсвоитьполютаблицы nil, чтобы его удалить. Это не совпадение: Lua хранитглобальныепеременныевобыкновенныхтаблицах.Мырассмотримэтоподробнеевглаве14.

Дляпредставлениязаписейвыиспользуетеимяполякакиндекс.Luaподдерживает это представление, предлагая a.name в качествесинтаксического сахара для a["name"]. Таким образом, мы можемпереписатьпоследниестрокипредыдущегопримераследующим,болеечистымобразом:

a.x=10--тоже,чтоиa["x"]=10

print(a.x)--тоже,чтоиprint(a["x"])

print(a.y)--тоже,чтоиprint(a["y"])

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

Типичная ошибка новичков — спутать а.х с а[х]. Первая формасоответствует а["х"], то есть таблица индексирована при помощистроки "х". Вторая форма означает, что таблица индексирована припомощизначенияпеременнойх.Взглянитенаразницу:

a={}

x="y"

a[x]=10--записывает10вполе"y"

Page 38: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(a[x])-->10--значениеполя"y"

print(a.x)-->nil--значениеполя"x"(неопределено)

print(a.y)-->10--значениеполя"y"

Чтобы представить традиционный массив или список, простоиспользуйте таблицу с целочисленными ключами. Нет ни способа, нинеобходимости объявлять размер; вы всего лишь инициализируете теэлементы,которыевамнужны:

--считывает10строк,сохраняяихвтаблице

a={}

fori=1,10do

a[i]=io.read()

end

Поскольку вы можете индексировать таблицу любым значением, выможете начинать индексы массива с любого числа, которое вамнравится. Однако, в Lua принято начинать массивы с единицы (а не снуля, как в С), и некоторые средства Lua придерживаются этогосоглашения.

Обычно,когдавыработаетесосписком,вамнужнознатьегодлину.Онаможетбытьконстантойилихранитьсягде-тоеще.Частомыхранимдлину списка в нечисловом поле таблицы; по историческим причинамнекоторыепрограммыиспользуютдляэтихцелейполе"n".

Однако, зачастую длина не может быть явно определена. Как выпомните, любой неинициализированный индекс равен nil; вы можетеиспользоватьэтозначениекакграничнуюметкудляобозначенияконцасписка. Например, после считывания десяти строк в список легкозапомнить,чтоегодлинаравна10,посколькуегочисловымиключамиявляются1,2,...,10.Этотприемработаеттолькотогда,когдаусписканет дыр, т.е. элементов nil внутри него. Мы называем такой списокпоследовательностью(sequence).

Для последовательностей Lua предлагает операцию длины '#'. Онавозвращает последний индекс или длину последовательности.Например, вы можете напечатать строки, cчитанные в предыдущемпримере,припомощиследующегокода:

--печатаетстроки

fori=1,#ado

print(a[i])

end

Поскольку мы можем индексировать таблицу с помощью любоготипа,топрииндексированиитаблицывозникаюттежетонкости,чтои

Page 39: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

припроверкенаравенство.Хотямыможеминдексироватьтаблицуиспомощью целого числа 0, и с помощью строки "0", эти два значенияразличны и тем самым соответствуют разным записям таблицы.Аналогично,строки"+1","01"и"1"такжесоответствуютразнымзаписямтаблицы. Когда вы не уверены насчет действительных типов вашихиндексов,дляверностииспользуйтеявноеприведениетипов:

i=10;j="10";k="+10"

a={}

a[i]="onevalue"

a[j]="anothervalue"

a[k]="yetanothervalue"

print(a[i])-->onevalue

print(a[j])-->anothervalue

print(a[k])-->yetanothervalue

print(a[tonumber(j)])-->onevalue

print(a[tonumber(k)])-->onevalue

Ввашейпрограммемогутпоявиться трудноуловимыеошибки, еслинеуделятьвниманиеданномумоменту.

2.6.Функции(function)

Функции (тип function) являются в Lua значениями первого класса:программымогутхранитьфункциивпеременных,передаватьфункциикак аргументы для других функций и возвращать функции какрезультаты.Подобныевозможностипридаютязыкуогромнуюгибкость;программа может переопределить функцию, чтобы добавить новуюфункциональность, или просто стереть функцию для созданиябезопасного окружения при выполнении фрагмента ненадежного кода(например, кода, полученного по сети). Более того, Lua предоставляетхорошую поддержку функционального программирования, включаявложенные функции с соответствующим лексическим окружением;просто дождитесь главы 6. Наконец, функции первого класса играютключевуюрольвобъектно-ориентированныхвозможностяхLua,какмыувидимвглаве16.

Lua может вызывать функции, написанные на Lua, и функции,написанныенаС.ОбычномыприбегаемкфункциямСдлядостиженияболее высокого быстродействия и для доступа к средствам,недоступным непосредственно из Lua, таким как средстваоперационнойсистемы.ВсестандартныебиблиотекивLuaнаписанынаС.Они включают в себяфункции обработки строк, обработки таблиц,

Page 40: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ввода-вывода, доступа к базовым средствам операционной системы,математическиеиотладочныефункции.

МыобсудимфункцииLuaвглаве5,афункцииСвглаве27.

2.7.Пользовательскиеданные(userdata)

Тип userdata позволяет запоминать произвольные данные С впеременных Lua. У него нет предопределенных операций в Lua, заисключениемприсваиванияипроверкинаравенство.Пользовательскиеданные используются для представления новых типов, созданныхприкладнойпрограммойилибиблиотекой, написаннойнаС; например,стандартнаябиблиотекаввода-выводаиспользуетихдляпредставленияоткрытыхфайлов.Мы более подробно обсудим этот тип позже, когдаперейдемкСAPI.

2.8.Нити(thread)

Тип thread будет разобран в главе 9, где мы рассмотримсопрограммы.

Упражнения

Упражнение 2.1. Что является значением выражения type(nil)==nil?(ВыможетеиспользоватьLuaдляпроверки своегоответа.)Можетеливыобъяснитьрезультат?

Упражнение 2.2. Какие из приведенных ниже чисел являютсядопустимымивLua?Каковыихзначения?

.0e12.e120.0e0x120xABFG0xAFFFF0xFFFFFFFF

0x0x1P100.1e10x0.1p1

Упражнение 2.3. Число 12.7 равно дроби 127/10, где знаменательявляется степенью десяти. Можете ли вы представить его в видепростойдроби,гдезнаменательявляетсястепеньюдвойки?Какнасчетчисла5.5?

Упражнение2.4.КаквыможетевставитьследующийфрагментXMLввидестрокивLua?

<![CDATA[

Helloworld

]]>

Page 41: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Используйтенеменеедвухразныхспособов.Упражнение 2.5. Допустим, вам нужно записать длинную

последовательность произвольных байт в виде строкового литерала вLua. Как вы это сделаете? Обратите внимание на такие моменты, какчитаемость,максимальнуюдлинустрокиибыстродействие.

Упражнение2.6.Рассмотритеследующийкод:а={};а.а=а

Чтобудет значениема.а.а.а?Какое-либоа в этойпоследовательностикак-тоотличаетсяотостальных?

Теперьдобавьтеследующуюстрокукпредыдущемукоду:а.а.а.а=3

Чтотеперьбудетзначениема.а.а.а?

Page 42: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА3

Выражения

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

3.1.Арифметическиеоперации

Luaподдерживаетстандартныеарифметическиеоперации:бинарные'+' (сложение), '-' (вычитание), '*' (умножение), '/' (деление), '^'(возведение в степень), '%' (остаток от деления) и унарную '-'(отрицание). Все они работают с вещественными числами. Например,х^0.5 вычисляет квадратныйкореньизх, ах^(-1/3) вычисляет обратнопропорциональноезначениеегокубическогокорня.

Следующееправилоопределяетоперациюостаткаотделения:а%b==а-math.floor(a/b)*b

Для целочисленных операндов она работает обычным образом и даетрезультатстемжезнаком,чтоиувторогоаргумента.Длявещественныхоперандовунееестьнекотороедополнительноеприменение.Например,х%1 дает дробную часть х, следовательно, х-х%1 дает его целую часть.Аналогично,х-х%0.01даетхровносдвумядесятичнымицифрамипослезапятой:

х=math.pi

print(х-х%0.01)-->3.14

В качестве другого примера применения операции остатка отделениядопустим,чтовамтребуетсяпроверить,будетлитранспортноесредство после поворота на заданный угол возвращаться в исходноеположение. Если угол задан в градусах, то вы можете использоватьследующуюформулу:

localtolerance=10

functionisturnback(angle)

Page 43: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

angle=angle%360

return(math.abs(angle-180)<tolerance)

end

Этоопределениеработаетдажедляотрицательныхуглов:print(isturnback(-180))-->true

Если нам потребуется работать с радианами вместо градусов, то мыпростоизменимконстантывнашейфункции:

localtolerance=0.17

functionisturnback(angle)

angle=angle%(2*math.pi)

return(math.abs(angle-math.pi)<tolerance)

end

Операция angle%(2*math.pi)— это все, что нам нужно для приведениялюбогоуглакзначениювинтервале[0,2π).

3.2.Операциисравнения

Luaпредоставляетследующиеоперациисравнения:<><=>===~=

Всеэтиоперациивсегдапроизводятбулевозначение.Операция == проверяет на равенство; операция ~= проверяет на

неравенство. Мы можем применять обе эти операции к любым двумзначениям.Еслизначенияобладаютразнымитипами,тоLuaсчитает,чтоонинеравны.ВпротивномслучаеLuaсравниваетихвсоответствиисихтипами.Вчастности,nilравнотолькосамомусебе.

Lua сравнивает таблицы и пользовательские данные по ссылке, тоестьдватакихзначениясчитаютсяравными,толькоеслиониявляютсяодним и тем же объектом. Например, после выполнения следующегокода:

a={};a.x=1;a.y=0

b={};b.x=1;b.y=0

c=a

мыполучима==с,ноа~=b.Мы можем применять операции порядка лишь к двум числам или

двум строкам. Lua сравнивает строки в алфавитном порядке, следуяустановленной для Lua локали. Например, для португальской локали

Page 44: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Latin-1мыполучим"acai"<"açaí"<"acorde". Значения, отличные отчиселистрок,можносравниватьтольконаравенство(инеравенство).

При сравнении значений разных типов нужно быть аккуратным:помните, что "0" отличается от 0. Более того, 2<15 очевидно истинно,но "2"<"15" ложно (из-за алфавитного порядка). Чтобы избежатьнепостоянных результатов, Lua выбрасывает ошибку, когда высмешиваетестрокиичислапривыявленииихпорядка,такомкак2<"15".

3.3.Логическиеоперации

Логические операции — это and, or и not. Как и управляющиеструктуры,вселогическиеоперациитрактуютfalseиnilкакложные,авсеостальные—какистинныезначения.Операцияandвозвращаетсвойпервый аргумент, если он ложный, иначе она возвращает свой второйаргумент. Операция or возвращает свой первый аргумент, если он неложный;иначеонавозвращаетсвойвторойаргумент:

print(4and5)-->5

print(niland13)-->nil

print(falseand13)-->false

print(4or5)-->4

print(falseor5)-->5

Обе операции, and и or, используют сокращенное вычисление, тоесть они вычисляют свой второй операнд только при необходимости.Сокращенное вычисление обеспечивает отсутствие ошибок во времявыполнения для выражений вроде (type(v) == "table" and v.tag ==

"h1"): Lua не будет пытаться вычислить v.tag, когда v не являетсятаблицей.

ПолезнойидиомойLuaявляетсях=хorv,котораяэквивалентнаifnotхthenх=vend

Toесть значениеx по умолчаниюустанавливается равным значениюv,еслихнеопределен(приусловии,чтохнеравенfalse).

Другойполезнойидиомойявляется(aandb)orсилипростоaandb or с, поскольку у and более высокий приоритет, чем у or. Онаэквивалентнавыражениюа?b:связыкеС,приусловиичтоb не ложно.Например,мыможемвыбратьмаксимумиздвухчиселхиуприпомощитакогооператора:

max=(х>у)andхorу

Page 45: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Когдах>у,первоевыражениесandистинно,поэтомуandвозвращаетсвое второе выражение (х), которое всегда истинно (поскольку эточисло), и затем выражение с or возвращает значение своего первоговыражения,х.Когдах>уложно,выражениесandтожеложно,поэтомуorвозвращаетсвоевтороевыражение,у.

Операцияnotвсегдавозвращаетбулевозначение:print(notnil)-->true

print(notfalse)-->true

print(not0)-->false

print(notnot1)-->true

print(notnotnil)-->false

3.4.Конкатенация

Lua обозначает операцию конкатенации строк как .. (две точки).Еслиодинизоперандовявляетсячислом,тоLuaпереведетеговстроку.(Некоторыеязыкииспользуютдляконкатенацииоперацию '+', новLua3+5отличаетсяот3..5.)

print("Hello".."World")-->HelloWorld

print(0..1)-->01

print(000..01)-->01

Помните, что строки в Lua являются неизменяемыми значениями.Операция конкатенации всегда создает новую строку, не изменяя своиоперанды:

a="Hello"

print(a.."World")-->HelloWorld

print(a)-->Hello

3.5.Операциядлины

Операциядлиныработаетсострокамиитаблицами.Сострокамионадает количество байт в строке. С таблицами она возвращает длинупредставленнойимипоследовательности.

Соперациейдлинысвязанонесколькораспространенныхидиомдляработыспоследовательностями:

print(a[#a])--печатаетпоследнеезначениепоследовательности

'a'

a[#a]=nil--удаляетэтопоследнеезначение

a[#a+1]=v--добавляет'v'кконцусписка

Page 46: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Какмывиделивпредыдущейглаве,операциядлинынепредсказуемадля списков с дырами (т.е. nil). Она работает только дляпоследовательностей, которые определены как списки без дыр. Болееточно, последовательность — это таблица, где числовые ключиобразуют множество 1,...,n для некоторого n. (Помните, что любойключ со значением nil на самом деле в таблице отсутствует.) Вчастности, таблица без числовых ключей — это последовательностьнулевойдлины.

С годами было много предложений по расширению значенияоперациидлинынаспискисдырами,ноэтолегческазать,чемсделать.Проблемавтом,чтопосколькусписокнасамомделеявляетсятаблицей,то понятие «длины» несколько расплывчато. Например, рассмотримсписок,получаемыйврезультатеследующегокода:

a={}

a[1]=1

a[2]=nil--ничегонеделает,таккакa[2]ужеnil

a[3]=1

a[4]=1

Легко сказать, что длина этого списка четыре, и у него есть дыра поиндексу2.Однако,чтоможносказатьоследующемпохожемпримере?

a={}

a[1]=1

a[10000]=1

Должнылимырассматриватьa как список с 10000 элементами, где9998изнихравныnil?Теперьпрограммаделаетследующее:

a[10000]=nil

Какова теперь длина этого списка? Должна ли она быть равна 9 999,посколькупрограммаудалилапоследнийэлемент?Илиможетбытьонапо-прежнему равна 10 000, так как программа всего лишь изменилазначение последнего элемента на nil? Или же длина должнауменьшитьсядо1?

Другое распространенное предложение — сделать так, чтобыоперация#возвращалаобщеечислоэлементоввтаблице.Этасемантикаясна и хорошо определена, но не несет в себе никакой пользы.Рассмотрим все предыдущие примеры и представим, насколькополезнойоказалосьбыподобнаяоперациядляалгоритмов,работающихсоспискамиилимассивами.

Page 47: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Ещеболеепроблемнымиявляютсязначенияnilвконцесписка.Какойдолжнабытьдлинаследующегосписка?

a={10,20,30,nil,nil}

Вспомним, что для Lua поле с nil не отличается от отсутствующегополя. Таким образом, предыдущая таблица эквивалентна {10,20,30}; еедлинаравна3,ане5.

Выможетесчитать,чтоnilвконцесписка—этокрайнеособенныйслучай.Однако,многиеспискистроятсяпутемдобавленияэлементовпоодному за раз. Любой список с дырами, построенный таким образом,долженбылиметьзначенияnilвсвоемконцевовремяпостроения.

Многие списки, которые мы используем в наших программах,являются последовательностями (например, строка файла не можетбыть nil), и поэтому большую часть времени применение операциидлиныбезопасно.Есливамдействительнонужнообрабатыватьспискисдырами,товыдолжныхранитьихдлинуявнымобразомгде-тоеще.

3.6.Приоритетыопераций

Приоритеты операций в Lua, от высшего к низшему, следуют этойтаблице:

^

not#-(унарный)

*/%

+-

..

<><=>=~===

and

or

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

a+i<b/2+1<-->(a+i)<((b/2)+1)

5+x^2*8<-->5+((x^2)*8)

a<yandy<=z<-->(a<y)and(y<=z)

-x^2<-->-(x^2)

x^y^z<-->x^(y^z)

Когдасомневаетесь,всегдаиспользуйтекруглыескобки.Этолегче,чем

Page 48: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

Конструкторытаблиц

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

Простейший конструктор — это пустой конструктор, {}, которыйсоздает пустую таблицу; мы уже видели его раньше. Конструкторытакжеинициализируютсписки.Например,оператор

days={"Sunday","Monday","Tuesday","Wednesday","Thursday",

"Friday","Saturday"}

проинициализирует days[1] строкой "Sunday" (первый элементконструктораимеетиндекс1,ане0),days[2]строкой"Monday"ит.д.:

print(days[4])-->Wednesday

Lua также предлагает специальный синтаксис для инициализациитаблиц,похожийназаписи,каквследующемпримере;

a={x=10,y=20}

Этастрокаэквивалентнаследующимкомандам:a={};a.x=10;a.y=20

Исходное выражение, тем не менее, проще и быстрее, поскольку Luaсразусоздаеттаблицусправильнымразмером.

Внезависимостиоттого,какимконструктороммыпользовалисьдлясозданиятаблицы,изнеевсегдаможнодобавлятьиудалятьполя:

w={x=0,y=0,label="console"}

x={math.sin(0),math.sin(1),math.sin(2)}

w[1]="anotherfield"--добавляетключ1втаблицу'w'

x.f=w--добавляетключ"f"втаблицу'x'

print(w["x"])-->0

print(w[1])-->anotherfield

print(x.f[1])-->anotherfield

w.x=nil--удаляетполе"x"

Однако, как я только что заметил, создание таблицы при помощиправильного конструктора более эффективно и, кроме того, более

Page 49: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

наглядно.Мыможемсмешиватьэтидвастиляинициализации(записиисписки)

водномитомжеконструкторе:polyline={color="blue",

thickness=2,

npoints=4,

{x=0,y=0},--polyline[1]

{x=-10,y=0},--polyline[2]

{x=-10,y=1},--polyline[3]

{x=0,y=1}--polyline[4]

}

Приведенный выше пример также показывает, как можно вкладыватьконструкторы один в другой для представления более сложныхструктур данных. Каждый из элементов polyline[i] — это таблица,представляющаясобойзапись:

print(polyline[2].x)-->-10

print(polyline[4].y)-->1

Уэтихдвухформконструктораестьсвоиограничения.Например,выне можете инициализировать поля отрицательными индексами илистроковыми индексами, которые не являются правильнымиидентификаторами.Длятакихцелейестьдругой,болееобщийформат.Вэтом формате мы явно пишем индекс, который должен бытьинициализированкаквыражение,междуквадратнымискобками:

opnames={["+"]="add",["-"]="sub",

["*"]="mul",["/"]="div"}

i=20;s="-"

a={[i+0]=s,[i+1]=s..s,[i+2]=s..s..s}

print(opnames[s])-->sub

print(a[22])-->---

Этотсинтаксисхотьиболеегромоздкий,ноприэтомболеегибкий:обеформы(встилесписковивстилезаписей)являютсячастнымислучаямиэтого более общего синтаксиса. Конструктор {x = 0, y = 0}

эквивалентен {["x"] = 0, ["y"] = 0}, а конструктор {"r", "g", "b"}

эквивалентен{[1]="r",[2]="g",[3]="b"}.Вы всегда можете поставить запятую после последней записи. Эти

замыкающиезапятыенеобязательны,новсегдадопустимы:a={[1]="red",[2]="green",[3]="blue",}

Page 50: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

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

{x=10,y=45;"one","two","three"}

Упражнения

Упражнение3.1.Чтонапечатаетследующаяпрограмма?fori=-10,10do

print(i,i%3)

end

Упражнение 3.2. Что является результатом выражения 2^3^4? А чтонасчет2^-3^4?

Упражнение3.3.Мыможемпредставитьмногочленanxn+an−1xn−1+...+a1x1+a0вLuaкаксписокегокоэффициентов{a0,a1,...,an}.

Напишитефункцию,котораяполучаетмногочлен(представленныйввидетаблицы)изначениеx,азатемвозвращаетзначениемногочлена.

Упражнение3.4.Можете ли вынаписатьфункциюизпредыдущегоупражнения так, чтобы использоватьn сложений иn умножений (и неиспользоватьвозведениевстепень)?

Упражнение 3.5. Как вы можете проверить, что значение являетсябулевым,неприбегаякфункцииtype?

Упражнение3.6.Рассмотримследующеевыражение:(xandyand(notz))or((noty)andx)

Нужны ли в этом выражении круглые скобки? Как бы выпосоветовалииспользоватьихвданномвыражении?

Упражнение3.7.Чтонапечатаетследующийскрипт?Объясните.sunday="monday";monday="sunday"

t={sunday="monday",[sunday]=monday}

print(t.sunday,t[sunday],t[t.sunday])

Упражнение3.8.Предположим,выхотитесоздатьтаблицу,котораясвязывает каждую экранированнуюпоследовательность для строк (см.

Page 51: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

раздел 2.4) с ее значением.Как бы вы написали конструктор для такойтаблицы?

Page 52: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА4

Операторы

Luaподдерживаетпочтитрадиционныйнабороператоров,похожихнаоператорывСилиPascal.Традиционныеоператорывключаютвсебяприсваивание, управляющие структуры и вызовы процедур. Lua такжеподдерживает менее распространенные операторы, такие какмножественноеприсваиваниеиобъявлениялокальныхпеременных.

4.1.Операторыприсваивания

Присваивание — это базовое средство изменения значенияпеременнойилиполятаблицы:

a="hello".."world"

t.n=t.n+1

Lua позволяет осуществлятьмножественное присваивание (multipleassignment),котороеприсваиваетсписокзначенийспискупеременныхзаодиншаг.Например,вприсваивании

a,b=10,2*x

переменнаяaполучаетзначение10,апеременнаяb—значение2*x.ВомножественномприсваиванииLuaспервавычисляетвсезначения

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

x,y=y,x--меняетместамизначения'x'с'y'

a[i],a[j]=a[j],a[i]--меняетместамизначения'a[i]'с

'a[j]'

Luaвсегдаприводитколичествозначенийкколичествупеременных:когда список значений короче списка переменных, дополнительныепеременные получают в качестве своих значений nil; когда длиннеесписокзначений,дополнительныезначенияпростоотбрасываются:

a,b,c=0,1

print(a,b,c)-->01nil

Page 53: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

a,b=a+1,b+1,b+2--значениеb+2игнорируется

print(a,b)-->12

a,b,c=0

print(a,b,c)-->0nilnil

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

a,b,c=0,0,0

print(a,b,c)-->000

В действительности большинство предыдущих примеров в чем-тоискусственно.Яредкоиспользуюмножественноеприсваиваниепросто,чтобы записать несколько не связанных между собой присваиваний водну строку. В частности, множественное присваивание не быстрееэквивалентных ему одиночных присваиваний. Тем не менее, зачастуюнам действительно требуется множественное присваивание. Мы ужевиделипример,меняющийместамизначенияудвухпеременных.Болеераспространенное использование заключается в получении несколькихзначенийпривызовефункции.Какмыподробнообсудимвразделе5.1,вызовфункцииможетвозвращатьнесколько значений.Втакихслучаяхединственноевыражениеможетпредоставитьзначениядлянесколькихпеременных.Например,вприсваиванииa,b=f()вызовf возвращаетдвазначения:aполучаетпервое,аbполучаетвторое.

4.2.Локальныепеременныеиблоки

Кромеглобальных,Luaподдерживаетилокальныепеременные.Мысоздаемлокальныепеременныеприпомощиоператораlocal:

j=10--глобальнаяпеременная

locali=1--локальнаяпеременная

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

x=10

locali=1--локальнаядлякуска

whilei<=xdo

localx=i*2--локальнаядлятелаwhile

Page 54: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(x)-->2,4,6,8,...

i=i+1

end

ifi>20then

localx--локальнаядлятела"then"

x=20

print(x+2)--(напечатает22,еслиусловиевыполнится)

else

print(x)-->10(глобальнаяпеременная)

end

print(x)-->10(глобальнаяпеременная)

Обратите внимание, что этот пример не будет работать так, как выожидаете, если ввести его в интерактивном режиме. В интерактивномрежиме каждая строка — это самостоятельный кусок (кроме случая,когда команда набрана не полностью). Как только вы введете вторуюстрокупримера(locali=1),Luaвыполнитееиначнетновыйкусоккодасо следующей строки. К тому моменту локальное объявление ужевыйдет из своей области видимости. Для решения этой проблемы мыможемявноограничитьвесьэтотблок,заключивегомеждуключевымисловамиdoиend.Кактольковывведетеdo,командазакончитсятольконасоответствующемемуend,поэтомуLuaнебудетпытатьсявыполнитькаждуюстрокупоотдельности.

Подобныеблокисdoудобныитогда,когдавамнуженболееточныйконтрольнадобластьювидимостинекоторыхлокальныхпеременных:

do

locala2=2*a

locald=(b^2-4*a*c)^(1/2)

x1=(-b+d)/a2

x2=(-b-d)/a2

end--областьвидимости'a2'и'd'заканчивается

здесь

print(x1,x2)

Хороший стиль программирования заключается в применениилокальныхпеременныхвезде,гдеэтовозможно.Локальныепеременныепомогают вам избежать засорения глобального окружения ненужнымиименами. Более того, доступ к локальной переменной быстрее, чем кглобальной. И наконец, локальная переменная перестает существовать,как только заканчивается ее область видимости, позволяя сборщикумусораосвободитьпамять,занимаемуюеезначением.

Luaобрабатываетобъявлениялокальныхпеременныхкакоператоры.

Page 55: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Такимобразом,выможетезаписыватьлокальныеобъявлениявезде,гдевы можете записать оператор. Область видимости объявленныхпеременныхначинаетсясразупослеобъявленияидлитсядоконцаблока.Каждое объявление может включать инициализирующее присваивание,которое действует так же, как и традиционное присваивание: лишниезначения отбрасываются, а лишние переменные получают значение nil.Если в объявлении нет инициализирующего присваивания, то оноинициализируетвсесвоипеременныезначениемnil:

locala,b=1,10

ifa<bthen

print(a)-->1

locala--неявное'=nil'

print(a)-->nil

end--завершаетблок,начатыйс'then'

print(a,b)-->110

ВLuaраспространенаследующаяидиома:localfoo=foo

Этот код создает локальную переменную foo и инициализирует еезначением глобальной переменной foo. (Локальная foo становитсявидимой только после этого объявления.) Эта идиома удобна, когдакуску необходимо предварительно сохранить первоначальное значениепеременной, даже если позже какая-нибудь другая функция изменитзначениеглобальнойfoo;этотакжеускоряетдоступкfoo.

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

4.3.Управляющиеконструкции

Luaпредоставляетнебольшойитрадиционныйнаборуправляющихструктур: if для условного выполнения, а while, repeat и for дляитерации. Все управляющие структуры обладают явным завершающимэлементом:endзавершаетif,forиwhile,аuntilзавершаетrepeat.

Page 56: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Условное выражение управляющей структуры может дать любоезначение. Помните о том, что Lua считает все значения, отличные отfalse и nil, истинными. (В частности, Lua считает истинными ноль ипустуюстроку.)

ifthenelse

Оператор if проверяет свое условие и в зависимости от результатавыполняет одну из своих частей, then или else. Часть с else являетсянеобязательной.

ifa<0thena=0end

ifa<bthenreturnaelsereturnbend

ifline>MAXLINESthen

showpage()

line=0

end

Длязаписивложенныхоператоровifвыможетеиспользоватьelseif.Этоаналогично else, за которым следует if, но при этом не возникаетнеобходимостивнесколькихend:

ifop=="+"then

r=a+b

elseifop=="-"then

r=a-b

elseifop=="*"then

r=a*b

elseifop=="/"then

r=a/b

else

error("invalidoperation")

end

Поскольку в Lua нет оператора switch, такие конструкции довольнораспространены.

while

Какследуетизназвания,whileповторяетсвоетелодотехпор,покаусловиеистинно.Какобычно,Luaспервапроверяетусловиеwhile;еслионо ложно, то цикл завершается; в противном случае Lua выполняет

Page 57: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

телоциклаиповторяетданныйпроцесс.

locali=1

whilea[i]do

print(a[i])

i=i+1

end

repeat

Какследуетизназвания,операторrepeat–untilповторяет свое телодо тех пор, пока условие не станет истинным. Данный операторпроизводит проверку условия после выполнения тела, поэтому телоциклабудетвыполненохотябыодинраз.

--печатаетпервуюнепустуювведеннуюстроку

repeat

line=io.read()

untilline~=""

print(line)

В отличие от многих других языков, в Lua условие входит в областьвидимостилокальнойпеременной,объявленнойвнутрицикла:

localsqr=x/2

repeat

sqr=(sqr+x/sqr)/2

localerror=math.abs(sqr^2-x)

untilerror<x/10000--local'error'stillvisiblehere

Числовойfor

Операторforсуществуетвдвухвариантах:числовойforиобщийfor.Числовойforимеетследующийсинтаксис:forvar=exp1,exp2,exp3do

<что-либо>end

Этотциклбудетвыполнятьчто-либодлякаждогозначенияvarотexp1до ехр2, используя ехр3 как шаг для увеличения var. Это третьевыражениенеобязательно; когдаоноотсутствует,Lua считает значениешагаравным1.Вкачестветипичныхпримеровможнопривести

fori=1,f(x)doprint(i)end

Page 58: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

fori=10,1,-1doprint(i)end

Есливамнуженциклбезверхнегопредела,товыможетеиспользоватьконстантуmath.huge:

fori=1,math.hugedo

if(0.3*i^3-20*i^2-500>=0)then

print(i)

break

end

end

Уциклаforестьнекоторыетонкости,которыевамжелательнознать,чтобы применять его наиболее эффективно. Во-первых, все тривыражениявычисляютсятолькоодинраз,передначаломцикла.Скажем,в нашем первом примере Lua вызовет f(х) всего один раз. Во-вторых,управляющая переменная является локальной переменной,автоматически объявляемой оператором for, и она видна лишь внутрицикла. Типичная ошибка — полагать, что эта переменная все ещесуществуетпослеокончанияцикла:

fori=1,10doprint(i)end

max=i--вероятно,неправильно!здесь'i'глобальная

Если вам нужно значение управляющей переменной после цикла(обычно когда вы прерываете цикл), то вы должны сохранить еезначениевдругойпеременной:

--находитзначениевсписке

localfound=nil

fori=1,#ado

ifa[i]<0then

found=i--сохраняетзначение'i'

break

end

end

print(found)

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

Общийfor

Page 59: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Общий for обходит все значения, возвращаемые итерирующейфункцией:

--печатаетвсезначениятаблицы't'

fork,vinpairs(t)doprint(k,v)end

Этот пример использует pairs, удобную итерирующую функцию дляперебора таблицы, предоставляемую базовой библиотекой Lua. Накаждом шаге этого цикла k получает ключ, a v получает значение,связанноесэтимключом.

Несмотря на свою кажущуюся простоту, общий for обладаетширокими возможностями. С подходящими итераторами вы можете влегкочитаемой форме перебрать практически все, что угодно.Стандартные библиотеки предоставляют несколько итераторов,позволяющих нам перебирать строки файла (io.lines), пары таблицы(pairs), элементы последовательности (ipars), слова внутри строки(string.gmatch)ит.д.

Конечно,мыможемнаписатьинаши собственныеитераторы.Хотяиспользовать общий for легко, у задачи написания итерирующихфункцийестьсвоинюансы,поэтомумырассмотримэтутемупозже,вглаве7.

У общего и числового циклов есть два одинаковых свойства:переменные цикла локальны для тела цикла, и вы никогда не должныприсваиватьимкакие-либозначения.

Рассмотрим более конкретный пример применения общего for.Допустим,увасестьтаблицасназваниямиднейнедели:

days={"Sunday","Monday","Tuesday","Wednesday",

"Thursday","Friday","Saturday"}

Теперьвыхотитеперевестиназваниеднявегономерпосчетувнеделе.Выможетеискатьзаданноеимявтаблице.Однако,каквыскороузнаете,вLuaпоискприменяетсяредко.Болееэффективнымподходомявляетсяпостроениеобратнойтаблицы,скажем,revDays,вкоторойназванияднейиспользуются как индексы, а номера используются как значения. Этатаблицавыгляделабыследующимобразом:

revDays={["Sunday"]=1,["Monday"]=2,

["Tuesday"]=3,["Wednesday"]=4,

["Thursday"]=5,["Friday"]=6,

["Saturday"]=7}

Тогдавсе,чтовамнужно,чтобынайтиномердняпоназванию,—это

Page 60: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

обратитьсяпоиндексуэтойобратнойтаблицы:x="Tuesday"

print(revDays[x])-->3

Разумеется,намнетребуетсяобъявлятьэтуобратнуютаблицувручную.Мыможемавтоматическипостроитьееизпервоначальной:

revDays={}

fork,vinpairs(days)do

revDays[v]=k

end

Этот цикл выполнит присваивание для каждого элемента days, гдепеременнаяkполучитключ(1,2,...),avполучитзначение("Sunday","Monday",...).

4.4.break,returnиgoto

Операторыbreakиreturnпозволяютнамвыпрыгнуть(осуществитьбезусловныйпереход)изблока.Операторgotoпозволяетнампрыгнутьпрактическивлюбуюточкуфункции.

Мыиспользуемоператорbreakдлязавершенияцикла.Этотоператорпрерывает внутренний цикл (for, repeat или while), который егосодержит; он не может быть использован за пределами цикла. Послепрерывания программа продолжит выполнение с точки, следующейсразупослепрерванногоцикла.

Оператор return возвращает результаты из функции, если они есть,или просто завершает ее. В конце каждой функции присутствуетнеявный возврат из нее, поэтому вам необязательно его использовать,если ваша функция завершается естественным образом, не возвращаяникакогозначения.

Изсинтаксическихсоображенийоператорreturnможетбытьтолькопоследним оператором блока: другими словами, либо последнимоператором в вашем куске, либо прямо перед end, else или until. Вследующемпримереreturn—этопоследнийоператорблокаthen.

locali=1

whilea[i]do

ifa[i]==vthenreturniend

i=i+1

end

Page 61: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Обычно это именно те места, где мы используем return, посколькулюбые другие операторы, следующие за ним, были бы недоступны.Иногда,темнеменее,удобнописатьreturnвсерединеблока;например,вы можете отлаживать функцию и хотите избежать ее выполнения. Вподобных случаях вы можете использовать явный блок do вокругоператораreturn:

functionfoo()

return--<<СИНТАКСИЧЕСКАЯОШИБКА

--'return'являетсяпоследнимоператоромвследующемкуске

doreturnend--OK

<другиеоператоры>end

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

В Lua синтаксис для оператора goto вполне традиционный: этозарезервированное слово goto, за которым следует имяметки, котороеможет быть любым допустимым идентификатором. Синтаксис дляметкинемногоболее запутанный: два двоеточия, за которыми следуетимя метки, после которого идут еще два двоеточия, например, какв ::name::. Эта сложность намеренная, чтобы заставить программистадваждыподумать,преждечемиспользоватьgoto.

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

Типичным и хорошо зарекомендовавшим себя применением gotoявляетсямоделированиенекоторойконструкции,которуювыузналииздругого языка, но которая отсутствует в Lua, например continue,многоуровневые break, redo и т. п. Оператор continue — это просто

Page 62: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

безусловный переход к метке в конце блока цикла; оператор redoосуществляетпереходкначалублока:

whilesome_conditiondo

::redo::

ifsome_other_conditionthengotocontinue

elseifyet_another_conditionthengotoredo

end

<какой-нибудькод>::continue::

end

Полезной деталью в спецификации Lua является то, что областьвидимости локальной переменной заканчивается на первом непустомоператоре блока, в котором эта переменная определена; меткисчитаются пустыми операторами. Чтобы увидеть полезность даннойдетали,рассмотримследующийфрагменткода:

whilesome_conditiondo

ifsome_other_conditionthengotocontinueend

localvar=something

<какой-нибудькод>::continue::

end

Вы можете подумать, что этот goto перепрыгивает в областьвидимости переменной var. Однако, метка continue находится послепоследнего непустого оператора блока, и поэтому не в областивидимостипеременнойvar.

Оператор goto также полезен при написании конечных автоматов(машин состояний). В качестве примера листинг 4.1 показываетпрограмму, проверяющую, содержит ли ее ввод четное количествонулей. Существуют лучшие способы написания этой специфическойпрограммы, но данный подход удобен, если вы хотите автоматическиперевести конечный автомат в код Lua (подумайте о динамическойгенерациикода).

Листинг4.1.Примерконечногоавтоматасиспользованиемgoto

::s1::do

localc=io.read(1)

ifc=='0'thengotos2

elseifc==nilthenprint'ok';return

elsegotos1

Page 63: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

end

end

::s2::do

localc=io.read(1)

ifc=='0'thengotos1

elseifc==nilthenprint'notok';return

elsegotos2

end

end

gotos1

В качестве другого примера рассмотрим простую игру в лабиринт.Лабиринт содержит несколько комнат, в каждой до четырех дверей:север, юг, восток и запад. На каждом шаге пользователь вводитнаправление движения. Если в этом направлении есть дверь, топользовательпереходитв соответствующуюкомнату;иначепрограммапечатаетпредупреждение.Цельюявляетсядойтиотначальнойкомнатыдоконечной.

Эта игра является типичной машиной состояний, где текущаякомната является состоянием. Мы можем реализовать этот лабиринт,используяодинблокдлякаждойкомнатыиоператорgotoдляпереходаиз одной комнаты в другую. Листинг 4.2 показывает, как можнонаписатьнебольшойлабиринтсчетыремякомнатами.

Для этой простой игры вы можете решить, что программа,управляемаяданными,вкоторойвыописываетекомнатыиперемещенияприпомощитаблиц,являетсяболееудачнымрешением.Однако,есливигре для каждой комнаты предусмотрены особые ситуации, то этотподходнаосновеконечногоавтоматаявляетсявполнеуместным.

Листинг4.2.Игра«лабиринт»gotoroom1--начальнаякомната

::room1::do

localmove=io.read()

ifmove=="south"thengotoroom3

elseifmove=="east"thengotoroom2

else

print("invalidmove")

gotoroom1--остаемсявэтойжекомнате

end

end

::room2::do

Page 64: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localmove=io.read()

ifmove=="south"thengotoroom4

elseifmove=="west"thengotoroom1

else

print("invalidmove")

gotoroom2

end

end

::room3::do

localmove=io.read()

ifmove=="north"thengotoroom1

elseifmove=="east"thengotoroom4

else

print("invalidmove")

gotoroom3

end

end

::room4::do

print("Congratulations,youwon!")

end

Упражнения

Упражнение4.1.БольшинствоязыковсС-образнымсинтаксисомнепредлагаетконструкциюelseif.ПочемуэтаконструкциябольшенужнавLua,чемвтехязыках?

Упражнение 4.2. Опишите четыре различных способа написатьбезусловныйциклвLua.Какойспособпредпочитаетевы?

Упражнение 4.3. Многие считают, что repeat—until используетсяредкоипотомунедолженприсутствоватьвминималистическихязыкахвродеLua.Чтывыдумаетеобэтом?

Упражнение4.4.Перепишитеконечныйавтоматизлистинга4.2безиспользованияgoto.

Упражнение 4.5. Можете ли вы объяснить, почему в Luaприсутствует ограничение на то, что goto не может выпрыгнуть изфункции?(Подсказка:какбывыреализовалиданнуювозможность?)

Упражнение4.6.Допустив,чтоgotoможетвыпрыгнутьизфункции,объясните, что делала бы программа в листинге 4.3. (Попытайтесьрассуждатьометкеприпомощитехжеправилобластивидимости,чтоиспользуютсядлялокальныхпеременных.)

Листинг 4.3. Странное (и недопустимое) использование

Page 65: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

goto

functiongetlabel()

returnfunction()gotoL1end

::L1::

return0

end

functionf(n)

ifn==0thenreturngetlabel()

else

localres=f(n-1)

print(n)

returnres

end

end

x=f(10)

x()

Page 66: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА5

Функции

Функции являются основныммеханизмом абстракции операторов ивыражений в Lua. Функции могут выполнять определенное задание(иногданазываемоепроцедуройилиподпрограммойвдругихязыках)иливычислятьивозвращатьзначения.Впервомслучаемыиспользуемвызовфункции как оператор; во втором случае мы используем его каквыражение:

print(8*9,9/8)

a=math.sin(3)+math.cos(10)

print(os.date())

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

print"HelloWorld"<-->print("HelloWorld")

dofile'a.lua'<-->dofile('a.lua')

print[[amulti-line<-->print([[amulti-line

message]]<-->message]])

f{x=10,y=20}<-->f({x=10,y=20})

type{}<-->type({})

Lua также предлагает специальный синтаксис для объектно-ориентированных вызовов — операцию двоеточия. Выражение вродео:foo(x)—этовсеголишьдругойспособнаписатьo.foo(o,x), то естьвызвать o.foo, добавляя o в качестве первого дополнительногоаргумента. В главе 16 мы обсудим подобные вызовы (и объектно-ориентированноепрограммирование)болееподробно.

ПрограммаLuaможетиспользоватьфункции,написанныекакнаLua,такинаС (илилюбомдругомязыке, которыйиспользуетсяосновнымприложением).Например, всефункции из стандартной библиотекиLuaнаписаны на С. Тем не менее, при вызове нет никакой разницы междуфункциями,определеннымивLua,ифункциями,определеннымивС.

Page 67: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Как мы видели в других примерах, определение функции следуеттрадиционномусинтаксису,например,какпоказанониже:

--складываетэлементыпоследовательности'a'

functionadd(a)

localsum=0

fori=1,#ado

sum=sum+a[i]

end

returnsum

end

Вэтом синтаксисе определениефункции содержитимя (в примере этоadd),списокпараметровитело,котороеявляетсяспискомоператоров.

Параметры работают в точности как локальные переменные,проинициализированные значениями аргументов, которые былипереданы вызову функции. Вы можете вызвать функцию с числомаргументов, отличным от ее числа параметров. Lua приведет числоаргументов к числу параметров так же, как и при множественномприсваивании: лишние аргументы отбрасываются, лишние параметрыполучаютnil.Например,рассмотримследующуюфункцию:

functionf(a,b)print(a,b)end

Онаобладаетследующимповедением:f(3)-->3nil

f(3,4)-->34

f(3,4,5)-->34(5отбрасывается)

Хотя подобное поведение может привести к ошибкампрограммирования (легко обнаруживаемым во время выполнения), онодовольно удобно, особенно для аргументов по умолчанию.Например,рассмотрим следующую функцию, инкрементирующую глобальныйсчетчик:

functionincCount(n)

n=nor1

count=count+n

end

Уэтойфункции1служитвкачествеаргументапоумолчанию;т.е.вызовincCount() без аргументов увеличит count на единицу. Когда вывызываете incCount(), Lua сперва инициализирует n значением nil;выражение с or возвращает свой второй операнд, и в результате Luaприсваиваетпеременнойnстандартноезначение1.

Page 68: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

5.1.Множественныерезультаты

Нетрадиционной,нодовольноудобнойособенностьюLuaявляетсято, что функции могут возвращать несколько значений. Так делаютнекоторые предопределенные функции в Lua. Примером являетсяфункцияstring.find,котораянаходитместоположениеобразцавстроке.Эта функция возвращает два индекса, когда находит образец: индекссимвола,скоторогоначинаетсяобразец,ииндекссимвола,накоторомон заканчивается. Множественное присваивание позволяет программеполучитьобарезультата:

s,e=string.find("helloLuausers","Lua")

print(s,e)-->79

(Обратитевнимание,чтоиндекспервогосимволастрокиравен1.)Функции, которые мы пишем в Lua, также могут возвращать

множественные результаты при помощи перечисления их послеключевого слова return. Например, функция для нахождениямаксимального элемента в последовательности может возвращать имаксимальныйэлемент,иегоместонахождение:

functionmaximum(a)

localmi=1--индексмаксимальногозначения

localm=a[mi]--максимальноезначение

fori=1,#ado

ifa[i]>mthen

mi=i;m=a[i]

end

end

returnm,mi

end

print(maximum({8,10,23,12,5}))-->233

Lua всегда приводит количество результатов функции кобстоятельствамеевызова.Когдамывызываемфункциюкакоператор,Lua отбрасывает все результаты функции. Когда мы используем вызовкаквыражение,Luaоставляеттолькопервыйрезультат.Мыполучаемвсерезультаты лишь тогда, когда вызов является последним (илиединственным) выражением в списке выражений. В Lua эти спискивстречаются в четырех конструкциях: множественные присваивания,аргументы в вызовах функций, конструкторы таблиц и операторыreturn. Для иллюстрации всех этих случаев допустим, что у нас естьтакиеопределениядлянашихпоследующихпримеров:

Page 69: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionfoo0()end--возвращает0значений

functionfoo1()return"a"end--возвращает1значение

functionfoo2()return"a","b"end--возвращает2значения

При множественном присваивании вызов функции в качествепоследнего (или единственного) выражения произведет столькорезультатов,сколькоунаспеременных:

x,y=foo2()--x="a",y="b"

x=foo2()--x="a","b"отбрасывается

x,y,z=10,foo2()--x=10,y="a",z="b"

Если функция не возвращает значения или возвращает их меньше, чемнужно,тоLuaпроизведетвкачественедостающихзначенийnil:

x,y=foo0()--x=nil,y=nil

x,y=foo1()--x="a",y=nil

x,y,z=foo2()--x="a",y="b",z=nil

Вызов функции, который не является последним элементом в списке,всегдадаетровноодинрезультат:

x,y=foo2(),20--x="a",y=20

x,y=foo0(),20,30--x=nil,y=20,30отбрасывается

Когда вызов функции является последним (или единственным)аргументом другого вызова, то все результаты первого вызовапередаются как аргументыдля второго.Мыуже виделипримеры этойконструкциисфункциейprint.Посколькуфункцияprintможетполучатьпеременное число аргументов, оператор print (g()) печатает всерезультаты,возвращенныефункциейg.

print(foo0())-->

print(foo1())-->a

print(foo2())-->ab

print(foo2(),1)-->a1

print(foo2().."x")-->ax(смотритедалее)

Когдавызовфункцииfoo2происходитвнутривыражения,Luaприводитчислорезультатовкодному;поэтомувпоследнейстрокеконкатенацияиспользуеттолько"а".

Еслимынапишемf(g(х)),гдеуfфиксированноечислоаргументов,тоLuaприведетчислорезультатовg кчислупараметровf, какмыужевиделиранее.

Конструктор таблицы тоже собирает все результаты вызова, безкаких-либоизменений:

Page 70: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

t={foo0()}--t={}(пустаятаблица)

t={foo1()}--t={"a"}

t={foo2()}--t={"a","b"}

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

t={foo0(),foo2(),4}--t[1]=nil,t[2]="a",t[3]=4

Наконец, оператор наподобие return f() возвращает все значения,возвращаемыеf:

functionfoo(i)

ifi==0thenreturnfoo0()

elseifi==1thenreturnfoo1()

elseifi==2thenreturnfoo2()

end

end

print(foo(1))-->a

print(foo(2))-->ab

print(foo(0))--(безрезультатов)

print(foo(3))--(безрезультатов)

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

print((foo0()))-->nil

print((foo1()))-->a

print((foo2()))-->a

Будьтевнимательны,таккакоператорreturnнетребуетскобоквокругвозвращаемого значения; любая пара круглых скобок, помещенная там,считаетсядополнительной.Поэтомуоператорвродеreturn(f(х))всегдавозвращает ровно одно значение, вне зависимости от того, сколькозначенийвозвращаетфункцияf.Иногдаэтоименното,чтовамнужно,иногданет.

Специальнойфункцией,возвращающейнесколькозначений,являетсяtable.unpack.Онаполучаетмассививозвращаетвкачестверезультатоввсеэлементыэтогомассива,начинаясиндекса1:

print(table.unpack{10,20,30})-->102030

a,b=table.unpack{10,20,30}--a=10,b=20,30отбрасывается

Одна из важных областей применения unpack — механизм общеговызова. Механизм общего вызова позволяет вам динамически вызвать

Page 71: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

любую функцию с любыми аргументами. В ANSI С, например, нетспособа написать код общего вызова. Вы можете объявить функцию,котораяполучаетпеременноечислоаргументов(припомощиstdarg.h),и выможете вызватьфункциюснесколькимипеременными,используяуказатели на функции. Однако, вы не можете вызвать функцию спеременнымчисломаргументов:укаждоговызова,которыйвыпишетена С, есть фиксированное число аргументов, а каждый аргументобладает фиксированным типом. В Lua, если вы хотите вызватьфункциюf спеременнымчисломаргументов, хранящимсявмассивеа,выпростопишитетак:

f(table.unpack(a))

Вызов unpack возвращает все значения из а, которые становятсяаргументамивызоваf.Например,рассмотримследующийвызов:

print(string.find("hello","ll"))

Выможетединамическипостроить эквивалентныйвызовприпомощиследующегокода:

f=string.find

a={"hello","ll"}

print(f(table.unpack(a)))

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

print(table.unpack({"Sun","Mon","Tue","Wed"},2,3))

-->MonTue

ХотяпредопределеннаяфункцияunpackнаписананаС,мымоглибынаписатьеенаLua,используярекурсию:

functionunpack(t,i,n)

i=ior1

n=nor#t

ifi<=nthen

returnt[i],unpack(t,i+1,n)

end

end

Первый раз, когда мы вызываем ее с единственным аргументом, i

Page 72: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

получает 1, а n получает длину последовательности. Затем функциявозвращаетt[1]вместесовсемирезультатамиunpack(t,2,n),что,всвоюочередь, возвращает t[2] со всеми результаты unpack(t,3,n) и т. д.,останавливаясьпослеnэлементов.

5.2.Вариадическиефункции

Функция в Lua может быть вариадической (variadic), т.е. иметьпеременное число аргументов. Например, мы уже вызывали print содним,сдвумяиболеечемсдвумяаргументами.ХотяprintопределенавС,мыможемопределятьвариадическиефункцииивLua.

Вкачествепростогопримераследующаяфункциявозвращаетсуммувсехсвоихаргументов:

functionadd(...)

locals=0

fori,vinipairs{...}do

s=s+v

end

returns

end

print(add(3,4,10,25,12))-->54

Триточки(...)вспискепараметровуказываютнато,чтоэтафункцияявляется вариадической. При вызове этой функции Lua внутреннимобразом собирает все ее аргументы; мы называем эти собранныеаргументы дополнительными аргументами функции. Функция можетполучать доступ к своим дополнительным аргументам опять же припомощитрехточек,теперьужевкачествевыражения.Внашемпримеревыражение{...}производитмассивсовсемисобраннымиаргументами.Затемфункцияобходитмассивдлясложенияегоэлементов.

Мы называем выражение с ... выражением с переменным числомаргументов(varargexpression).Оноведетсебякакфункциясвозвратомнескольких значений, возвращая все дополнительные аргументытекущей функции. Например, команда print(...) напечатает вседополнительныеаргументытекущейфункции.Аналогично,следующаякомандасоздастдвелокальныепеременныесозначениямипервыхдвухнеобязательныхаргументов(илиnil,еслитакихаргументовнет).

locala,b=...

Насамомделемыможемимитироватьстандартныймеханизмпередачи

Page 73: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

параметроввLua,переводяfunctionfoo(a,b,c)

вfunctionfoo(...)

locala,b,c=...

Тем, кому нравится изящный механизм передачи параметров в Perl,можетпонравитьсяэтавтораяформа.

Функция, наподобие следующей, просто возвращает все своиаргументыпривызове:

functionid(...)return...end

Этомногозначнаяфункция тождества.Следующаяфункция ведет себятак же, как и другая функция foo, за исключением того, что передвызовомонапечатаетсообщениесосвоимиаргументами:

functionfoo1(...)

print("callingfoo:",...)

returnfoo(...)

end

Этополезныйтрюкдляотслеживаниявызововконкретнойфункции.Давайтерассмотримещеодинполезныйпример.Luaпредоставляет

отдельные функции для форматирования текста (string.format) и егозаписи (io.write). Несложно объединить обе функции в однувариадическую:

functionfwrite(fmt,...)

returnio.write(string.format(fmt,...))

end

Обратитевниманиенаприсутствиефиксированногопараметраfmtпередточками. Вариадические функции могут иметь любое количествофиксированных параметров перед своей вариадической частью. Luaприсваиваетэтимпараметрампервыеаргументы;остальные(еслиесть)идут как дополнительные параметры. Ниже мы покажем несколькопримероввызововизначениясоответствующихпараметров:

ВЫЗОВПАРАМЕТРЫ

fwrite()fmt=nil,бездополнительныхаргументов

fwrite("a")fmt="a",бездополнительныхаргументов

fwrite("%d%d",4,5)fmt="%d%d",доп.аргументы=4и5

Page 74: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

(Обратитевнимание,чтовызовfwrite()приведеткошибке,посколькуstring.formatтребуетстрокувкачествесвоегопервогоаргумента.)

Для обхода своих дополнительных аргументов функция можетиспользоватьвыражение{...},чтобысобратьихвсехвтаблицу,какмыэтосделаливнашемопределенииadd.

Однако, в тех редких случаях, когда дополнительные аргументымогут быть допустимыми nil, таблица, созданная при помощи {...},можетнебытьправильнойпоследовательностью.Например,длятакойтаблицы не существует способа узнать, были ли конечные nil висходных аргументах. Для этих случаев Lua предлагает функциюtable.pack (Примечание: Эта функция появилась только в Lua 5.2). Этафункцияполучаетпроизвольноечислоаргументовивозвращаетновуютаблицу со всеми своими аргументами, как и {...}, но в этой таблицебудетдополнительноеполе'n',содержащееполноечислоееаргументов.Следующаяфункцияиспользуетtable.pack,чтобыпроверить,чтосредиееаргументовнетnil:

functionnonils(...)

localarg=table.pack(...)

fori=1,arg.ndo

ifarg[i]==nilthenreturnfalseend

end

returntrue

end

print(nonils(2,3,nil))-->false

print(nonils(2,3))-->true

print(nonils())-->true

print(nonils(nil))-->false

Однако, не забывайте, что {...} быстрее и понятнее, чемtable.pack(...),когдасредидополнительныхаргументовнеможетбытьnil.

5.3.Именованныеаргументы

Механизм передачи параметров в Lua является позиционным: привызове функции аргументы сопоставляются с параметрамисоответственно их позициям. Первый аргумент дает значение первомупараметру и т. д. Тем не менее, иногда удобно задавать аргументы поимени.Чтобыпроиллюстрироватьданныймомент,давайтерассмотримфункциюos.rename (из библиотеки os), которая переименовывает файл.

Page 75: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

--недопустимыйкод

rename(old="temp.lua",new="temp1.lua")

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

rename{old="temp.lua",new="temp1.lua"}

Соответственно, мы переопределяем функцию rename только с однимпараметромиполучаемдействительныеаргументыизэтогопараметра:

functionrename(arg)

returnos.rename(arg.old,arg.new)

end

Этотспособпередачипараметровособенноудобен,когдауфункциимного аргументов и большинство из них необязательные. Например,функция, которая создает новое окно в библиотеке GUI, может иметьдесяткиаргументов,побольшейчастинеобязательных,которыелучшезадаватьприпомощиимен:

w=Window{x=0,y=0,width=300,height=200,

title="Lua",background="blue",

border=true

}

Функция Window затем вольна проверять обязательные аргументы,добавлять значения по умолчанию и т.п. Предположив, что у нас естьпримитивнаяфункция_Window,котораянасамомделесоздаетновоеокно(и которой все аргументы требуются в определенном порядке), мымоглибыопределитьWindowкакпоказановлистинге5.1.

Листинг 5.1. Функция с именованными необязательнымипараметрами

Page 76: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionWindow(options)

--проверкаобязательныхопций

iftype(options.title)~="string"then

error("notitle")

elseiftype(options.width)~="number"then

error("nowidth")

elseiftype(options.height)~="number"then

error("noheight")

end

--everythingelseisoptional

_Window(options.title,

options.xor0,--значениепоумолчанию

options.yor0,--значениепоумолчанию

options.width,options.height,

options.backgroundor"white",--поумолчанию

options.border--поумолчаниюfalse(nil)

)

end

Упражнения

Упражнение 5.1. Напишите функцию, которая получаетпроизвольноечислострокивозвращаетихсоединеннымивместе.

Упражнение 5.2. Напишите функцию, которая получает массив ипечатает все элементы этого массива. Рассмотрите преимущества инедостаткииспользованияtable.unpackвэтойфункции.

Упражнение 5.3. Напишите функцию, которая получаетпроизвольноечислозначенийивозвращаетихвсе,кромепервого.

Упражнение 5.4. Напишите функцию, которая получает массив ипечатает все комбинации элементов этого массива. (Подсказка: выможетеиспользоватьрекурсивнуюформулудляполучениякомбинаций:C (n, m) = C (n−1, m−1) + C (n−1, m). Для получения всех C(n, m)комбинацийизn элементов в группах размераm вы сперва добавляетепервыйэлементкрезультатуизатемгенерируетевсекомбинацииC(n−1,m−1)изоставшихсяэлементовнаоставшихсяпозициях.Когдаnменьше,чемm,комбинацийбольшенет.Когдаmравнонулю,существуеттолькооднакомбинация,ионанеиспользуетникакихэлементов.)

Page 77: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА6

Ещеразофункциях

Функции в Lua являются значениями первого класса ссоответствующейлексическойобластьювидимости.

Что для функций означает быть «значениями первого класса»? Этозначит, что в Lua функция — это значение, обладающее теми жеправами,чтоитрадиционныезначениявродечиселистрок.Мыможемхранитьфункции в переменных (локальных и глобальных) и таблицах,мыможемпередаватьфункциикакаргументыивозвращатьихиздругихфункций.

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

НесколькозапутаннымпонятиемвLuaявляетсято,чтофункции,каки другие значения, являются анонимными; у них нет имен. Когда мыговоримобименифункции,такойкакprint,насамомделемыимеемввиду переменную, которая хранит данную функцию. Как и с любойдругой переменной, хранящей любое другое значение, мы можемманипулировать этими переменными множеством способов.Следующийпример,хотьинемногонелепый,показываетэтотмомент:

a={p=print}

a.p("HelloWorld")-->HelloWorld

print=math.sin--'print'теперьссылаетсянафункциюsin

a.p(print(1))-->0.841470

sin=a.p--'sin'теперьссылаетсянафункциюprint

sin(10,20)-->1020

(Позжемыувидимполезныепримененияэтойвозможности.)Если функции являются значениями, то существуют ли выражения,

Page 78: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

которые создаютфункции?Да.По сути, стандартныйспособнаписатьфункциювLua,такойкак

functionfoo(x)return2*xend

являетсявсеголишьобразцомтого,чтомыназываемсинтаксическимсахаром;этопростоболеекрасивыйспособнаписатьследующийкод:

foo=function(x)return2*xend

Таким образом, определение функции — это по сути оператор(присваивание, если более точно), который создает значение типа"function" и присваивает его переменной. Мы можем рассматриватьвыражение function (х)тело end как конструктор функции, точно также, как {} является конструктором таблицы. Мы называем результатподобных конструкторовфункцийанонимнойфункцией. Хотя мы частоприсваиваемфункцииглобальнымпеременным,задаваяимчто-товродеимени, бывают случаи, когдафункцииостаются анонимными.Давайтерассмотримнесколькопримеров.

Табличная библиотека предоставляет функцию table.sort, котораяполучает таблицуи сортирует ее элементы.Подобнаяфункциядолжнапозволять бесконечные вариации порядка сортировки: по возрастаниюилипоубыванию,числовойилиалфавитный,таблицыссортировкойпоключу и т. д. Вместо попытки предоставить все виды опций, sortпредоставляет единственный необязательный параметр, которыйявляетсяпорядковойфункцией:функция,котораяпринимаетдваэлементаи определяет, должен ли первый элемент идти перед вторым вотсортированном списке. Например, допустим, что у нас есть такаятаблицазаписей:

network={

{name="grauna",IP="210.26.30.34"},

{name="arraial",IP="210.26.30.23"},

{name="lua",IP="210.26.23.12"},

{name="derain",IP="210.26.23.20"},

}

Если нам нужно отсортировать таблицу по полю name в обратномалфавитномпорядке,тодостаточнонаписатьтак:

table.sort(network,function(a,b)return(a.name>b.name)end)

Посмотрите,насколькоудобнаанонимнаяфункциявэтомоператоре.Функция,котораяполучаетдругуюфункциюкакаргумент,такаякак

Page 79: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

sort,являетсятем,чтомыназываемфункциейвысшегопорядка.Функциивысшего порядка являются эффективным механизмомпрограммирования, аиспользование анонимныхфункцийвкачествеихаргументов служит колоссальным источником гибкости. Однако,запомните, что функции высших порядков не являются чем-тоособенным; они — прямое следствие способности Lua работать сфункциямикаксозначениямипервогокласса.

Чтобы проиллюстрировать применение функций высших порядков,мы напишем упрощенное определение распространенной функциивысшегопорядка—производной.Следуянеформальномуопределению,производнаяфункцииfвточкех—этозначение(f(x+d)−f(x))/d,гдеdстремится к бесконечно малой величине. В соответствии с этимопределением мы можем вычислить приближенное значениепроизводнойследующимобразом:

functionderivative(f,delta)

delta=deltaor1e-4

returnfunction(x)

return(f(x+delta)-f(x))/delta

end

end

Еслизадатьфункциюf,вызовderivative(f)вернетеепроизводную(какприближенноезначение),котораяявляетсяещеоднойфункцией:

c=derivative(math.sin)

>print(math.cos(5.2),c(5.2))

-->0.468516671300380.46856084325086

print(math.cos(10),c(10))

-->-0.83907152907645-0.83904432662041

Поскольку функции в Lua являются значениями первого класса, мыможем хранить их не только в глобальных переменных, но и влокальныхпеременныхиполях таблиц.Какмыувидим в дальнейшем,использование функций в полях таблицы является ключевымкомпонентом для некоторых продвинутых областей применения Lua,такихкакмодулииобъектно-ориентированноепрограммирование.

6.1.Замыкания

Когда мы определяем одну функцию внутри другой, она получаетполныйдоступклокальнымпеременнымокружающейеефункции;мыназываемэтосвойстволексическойобластьювидимости(lexicalscoping).

Page 80: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

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

names={"Peter","Paul","Mary"}

grades={Mary=10,Paul=7,Peter=8}

table.sort(names,function(n1,n2)

returngrades[n1]>grades[n2]--сравниваетоценки

end)

Теперьдопустим,чтовамнужносоздатьфункциюдлярешенияданнойзадачи:

functionsortbygrade(names,grades)

table.sort(names,function(n1,n2)

returngrades[n1]>grades[n2]--сравниваетоценки

end)

end

Интересныммоментом в этом примере является то, что анонимнаяфункция, переданная функции sort, обращается к параметру grades,которыйявляетсялокальнымдляокружающейегофункцииsortbygrade.Внутри этой анонимной функции grades не является ни глобальнойпеременной,нилокальнойпеременной,аявляетсятем,чтомыназываемнелокальной переменной. (По историческим причинам нелокальныепеременныевLuaтакженазываютсяверхнимизначениями(upvalue).)

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

functionnewCounter()

locali=0

returnfunction()--анонимнаяфункция

i=i+1

returni

end

end

c1=newCounter()

print(c1())-->1

Page 81: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(c1())-->2

В этом коде анонимная функция ссылается на нелокальнуюпеременную i для хранения своего счетчика. Однако, к тому времени,какмывызовеманонимнуюфункцию,переменнаяiужевыйдетизсвоейобласти видимости, поскольку функция, которая создала этупеременную (newCounter), уже будет возвращена. Тем не менее, Luaправильнообрабатываетэтуситуацию,используяконцепциюзамыкания(closure).Прощеговоря,замыкание—этофункциявместесовсем,чтоейнужнодляправильногодоступакнелокальнымпеременным.ЕслимысновавызовемnewCounter,тоонасоздастновуюлокальнуюпеременнуюi, поэтому мы получим новое замыкание, действующее поверх новойпеременной:

c2=newCounter()

print(c2())-->1

print(c1())-->3

print(c2())-->2

Такимобразом,c1ис2—эторазныезамыканияповерходнойитойжефункции, и каждое замыкание воздействует на независимый экземплярлокальнойпеременнойi.

С технической точки зрения, значениемвLua является замыкание, анефункция.Самапосебефункция—этолишьпрототипдлязамыканий.Тем не менее, мы продолжим использовать термин «функция» дляобозначениязамыканиявезде,гдеэтонеприведеткпутанице.

Во многих случаях замыкания оказываются ценным инструментом.Какмыужевидели,ониудобнывкачествеаргументовфункцийвысшихпорядков,такихкакsort.Замыканиятакжеценныдляфункций,которыестроят другие функции, как в нашем примере с newCounter или спроизводной; этот механизм позволяет программам Lua внедрятьсовременные методы из мира функционального программирования.Замыкания также удобны для функций обратного вызова (callback).ЗдесьподходиттипичныйпримерпросозданиекнопоквтрадиционноминструментарииGUI.Укаждойкнопкиестьфункцияобратноговызова,котораядолжнабытьвызвана,когдапользовательнажимаетэтукнопку;вам требуется, чтобы разные кнопки совершали немного разныедействия при нажатии. Например, цифровому калькулятору нужнодесятьпохожихкнопок,пооднойнакаждуюцифру.Выможетесоздатькаждуюкнопкуспомощьюфункциинаподобиеэтой:

functiondigitButton(digit)

Page 82: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

returnButton{label=tostring(digit),

action=function()

add_to_display(digit)

end

}

end

В данном примере мы предполагаем, что Button — это функция изинструментария, которая создает новые кнопки; label — это меткакнопки; action — это замыкание обратного вызова, которое нужновызвать при нажатии кнопки.Обратный вызовможет быть произведенспустя длительное время после того, как digitButton выполнила своюзадачу,ипослетого,каклокальнаяпеременнаяdigitвышлаизобластивидимости, так как он по-прежнему может обращаться к этойпеременной.

Замыканиятакжеценнывсовсемдругомслучае.ПосколькуфункцииLua хранятся в обычных переменных, мы можем их легкопереопределять, даже если они встроенные.Эта возможность являетсяодной из причин, почему Lua столь гибок. Часто, когда выпереопределяете функцию, в новой реализации вам все равно нужнаизначальная функция. Например, предположим, что вы хотитепереопределитьфункциюsin, чтобы она работала с градусами вместорадиан. Эта новая функция конвертирует свой аргумент и затемвызывает исходную функцию sin для выполнения настоящей работы.Вашкодприэтомможетвыглядетьследующимобразом:

oldSin=math.sin

math.sin=function(x)

returnoldSin(x*math.pi/180)

end

Далее приведен немного более аккуратный способ выполнить этопереопределение:

do

localoldSin=math.sin

localk=math.pi/180

math.sin=function(x)

returnoldSin(x*k)

end

end

Теперь мы сохраняем старую версию в локальной переменной;единственныйспособобратитьсякней—черезновуюфункцию.

Вы можете воспользоваться этим подходом и для создания

Page 83: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

безопасных окружений, также называемых песочницами (sandbox).Безопасные окружения крайне важны при выполнении ненадежногокода, например, полученного сервером через Интернет. Скажем, чтобыограничитьфайлы,ккоторымпрограммаможетобратиться,мыможемпереопределитьфункциюio.open,используязамыкания:

do

localoldOpen=io.open

localaccess_OK=function(filename,mode)

<проверкадоступа>end

io.open=function(filename,mode)

ifaccess_OK(filename,mode)then

returnoldOpen(filename,mode)

else

returnnil,"accessdenied"

end

end

end

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

6.2.Неглобальныефункции

Очевидным следствием того, что функции являются значениямипервогокласса,являетсято,чтомыможемхранитьфункциинетольковглобальныхпеременных,ноивлокальныхпеременныхиполяхтаблицы.

Мы уже видели некоторые примерыфункций внутри полей таблиц:большинство библиотек Lua использует этот механизм (например,io.read, math.sin). Для создания подобных функций в Lua нам нужнопросто соединить стандартный синтаксис для функций с синтаксисомдлятаблиц:

Lib={}

Page 84: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Lib.foo=function(x,y)returnx+yend

Lib.goo=function(x,y)returnx-yend

print(Lib.foo(2,3),Lib.goo(2,3))-->5-1

Разумеется,мытакжеможемиспользоватьконструкторы:Lib={

foo=function(x,y)returnx+yend,

goo=function(x,y)returnx-yend

}

Кроме того, Lua предлагает еще один синтаксис для определенияподобныхфункций:

Lib={}

functionLib.foo(x,y)returnx+yend

functionLib.goo(x,y)returnx-yend

Когдамысохраняемфункциювлокальнойпеременной,мыполучаемлокальную функцию, то есть функцию с ограниченной областьювидимости. Подобные определения особенно удобны для пакетов:посколькуLuaрассматриваеткаждыйкусоккакфункцию,кусокможетобъявлять локальные функции, которые видны только внутри него.Лексическаяобласть видимостипозволяет другимфункциямизпакетаиспользоватьэтилокальныефункции:

localf=function(<параметры>)

<тело>end

localg=function(<параметры>)

<какой-нибудькод>f()--'f'здесьвидима

<какой-нибудькод>end

Lua поощряет подобное применение локальных функций посредствомсинтаксическегосахара:

localfunctionf(<params>)

<тело>

end

При определении рекурсивных локальных функций возникаеттребующийуточнениямомент.Деловтом,чтообычныйподходздесьнеработает.Рассмотримследующееопределение:

Page 85: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localfact=function(n)

ifn==0thenreturn1

elsereturnn*fact(n-1)--глюк

end

end

Когда Lua компилирует вызов fact(n—1) в теле функции, локальнаяфункция fact еще не определена. Поэтому данное выражениепопытается вызвать глобальную функцию fact, а не локальную. Мыможемрешитьэтупроблему,сперваопределивлокальнуюпеременную,азатемужесамуфункцию:

localfact

fact=function(n)

ifn==0thenreturn1

elsereturnn*fact(n-1)

end

end

Теперьfactвнутрифункцииссылаетсяналокальнуюпеременную.Еезначение при определении функции не важно; к моменту выполненияфункции,factужеполучитправильноезначение.

Когда Lua предлагает свой синтаксический сахар для локальныхфункций, он не использует простое определение. Вместо этогоопределениенаподобие

localfunctionfoo(<параметры>)<тело>end

расширяетсядо

localfoo;foo=function(<параметры>)<тело>end

Поэтому мы можем спокойно использовать этот синтаксис длярекурсивныхфункций.

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

localf,g--предварительныеобъявления

functiong()

<какой-нибудькод>f()<какой-нибудькод>end

functionf()

<какой-нибудькод>g()<какой-нибудькод>

Page 86: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

end

Остерегайтесь писать local function f в последнем определении.Иначе Lua создаст новую локальную переменную f, оставивизначальнуюf(ккоторойпривязанаg)неопределенной.

6.3.Корректныехвостовыевызовы

Другой интересной особенностью функций в Lua является то, чтоLua выполняет устранение хвостовых вызовов. (Это значит, что Luaподдерживаеткорректнуюхвостовуюрекурсию,хотяданноепонятиенесвязанонепосредственносрекурсией;см.упражнение6.3.)

Хвостовойвызов (tailcall)— этофактическиgoto, выглядящий каквызов функции. Хвостовой вызов происходит, когда одна функциявызываетдругуюв качестве своегопоследнегодействия, ипотому ейбольше нечего делать. Например, в следующем коде вызов функции gявляетсяхвостовым:

functionf(x)returng(x)end

Послетого,какf вызоветg, ей больше нечего делать.В подобныхситуациях программе не требуется возвращаться в вызывающуюфункциюпозавершениивызваннойфункции.Поэтомупослехвостовоговызова программе не нужно хранить какую-либо информацию овызывающей функции в стеке. Когда g возвращает управление, ономожет непосредственно перейти к моменту вызова f. Некоторыереализацииязыков,например,интерпретаторLua,извлекаютизданногофакта выгоду и на самом деле не используют какое-либодополнительноеместовстекеприсовершениихвостовоговызова.Мыговорим, что эти реализации поддерживают устранение хвостовыхвызовов(tail-callelimination).

Поскольку хвостовые вызовы не используют место в стеке,количество вложенных хвостовых вызовов, которое программа можетвыполнить, не ограничено. Скажем, мы можем вызвать следующуюфункцию,передавлюбоечисловкачествеаргумента:

functionfoo(n)

ifn>0thenreturnfoo(n-1)end

end

Этотвызовникогданеприведеткпереполнениюстека.Когда мы прибегаем к устранению хвостовых вызовов, возникает

Page 87: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

тонкий вопрос: какой именно вызов считать хвостовым? Некоторыевполнеочевидныекандидатынесоответствуюттребованиюотом,чтовызывающейфункциибольшенечегоделатьпослевызова.Например,вследующемкодевызовgнеявляетсяхвостовым:

functionf(x)g(x)end

Проблемавэтомпримересостоитвтом,чтопослевызоваgфункцияf должна отбросить результаты g перед возвратом. Все следующиевызовытакженеудовлетворяютданномутребованию:

returng(x)+1--необходимовыполнитьсложение

returnxorg(x)--необходимопривестикодномузначению

return(g(x))--необходимопривестикодномузначению

ВLuaхвостовымсчитаетсялишьвызоввидаreturnфункция(аргументы).Однако,ифункция,иееаргументымогутбытьсложнымивыражениями,посколькуLuaвычислитихпередвызовом.Например,следующийвызовявляетсяхвостовым:

returnx[i].foo(x[j]+a*b,i+j)

Упражнения

Упражнение 6.1. Напишите функцию integral, которая принимаетфункциюfивозвращаетприближенноезначениеееинтеграла.

Упражнение6.2.Вупражнении3.3вамнадобылонаписатьфункцию,которая получает многочлен (представленный в виде таблицы) изначениеегопеременной,азатемвозвращаетзначениеэтогомногочлена.Напишитекаррированную (принимающая свои аргументы по одному зараз) версию данной функции. Ваша функция должна приниматьмногочленивозвращатьфункцию,которая,будучивызванадлякакого-либозначениях,вернетзначениемногочленадляэтогох.Например:

f=newpoly({3,0,1})

print(f(0))-->1

print(f(5))-->76

print(f(10))-->301

Упражнение6.3.Иногдаязыкскорректнымихвостовымивызоваминазывают языком с поддержкой корректной хвостовой рекурсии,аргументируя это тем, что данное качество имеет значение толькотогда, когда унас есть рекурсивныевызовы (Без рекурсивныхвызовов

Page 88: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

максимальнаяглубинавызововбылабыстатическификсированной.)Покажите, что это утверждение не верно для динамически

типизированных языков вроде Lua: напишите программу, котораясовершает неограниченную цепочку вызовов без рекурсии. (Подсказка:см.раздел8.1.)

Упражнение 6.4. Как мы видели, хвостовой вызов — этозамаскированный goto. Придерживаясь этой идеи, перепишите кодпростой игры в лабиринт из раздела 4.4 с применением хвостовыхвызовов. Каждый блок должен стать новой функцией, а каждый gotoдолженстатьхвостовымвызовом.

Page 89: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА7

Итераторыиобщийfor

Вэтойглавемырассмотрим,какписатьитераторыдляобщего for.Начав с простых итераторов, мы узнаем, как использовать всю силуобщегоforдлянаписанияболеепростыхиэффективныхитераторов.

7.1.Итераторыизамыкания

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

Любой итератор должен где-то хранить свое состояние междупоследовательнымивызовами,чтобызнать,гдеоннаходитсяикаксебявестисэтогоместа.Замыканияпредоставляютвеликолепныймеханизмдля этой задачи. Вспомним, что замыкание — это функция, котораяобращается к одной или нескольким локальным переменным изохватывающего ее окружения. Эти переменные хранят свои значениямежду последовательными вызовами замыкания, позволяя тем самымзамыканию помнить, где оно находится при переборе элементов.Разумеется, для создания нового замыкания мы также должны создатьегонелокальныепеременные.Поэтому конструкция замыкания обычновключает в себя две функции: само замыкание ифабрику—функцию,котораясоздаетзамыканиевместесокружающимиеепеременными.

Вкачествепримерадавайтенапишемпростойитератордлясписка.Вотличие от ipairs, этот итератор возвращает не индекс каждогоэлемента,алишьегозначение:

functionvalues(t)

locali=0

returnfunction()i=i+1;returnt[i]end

end

В этом примере values — это фабрика. Каждый раз, когда мывызываемэтуфабрику,онасоздаетновоезамыкание(самитератор).Этозамыкание хранит свое состояние в своих внешних переменных t и i.

Page 90: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Каждый раз, когда мы вызываем этот итератор, он возвращаетследующее значение из списка t. После последнего элемента итераторвернетnil,чтоозначаетконецитерации.

Мыможемиспользоватьэтотитераторвциклеwhile:t={10,20,30}

iter=values(t)--создаетитератор

whiletruedo

localelement=iter()--вызываетитератор

ifelement==nilthenbreakend

print(element)

end

Темнеменее,гораздолегчеиспользоватьобщийfor.Вконцеконцов,онбылразработандляданноговидаитераций:

t={10,20,30}

forelementinvalues(t)do

print(element)

end

Общийforведетдляитерирующегоциклавсюбухгалтерию:онхранитвнутриитерирующуюфункцию,поэтомунамненужнапеременнаяiter;онвызываетитератордлякаждойновойитерации;ионостанавливаетцикл,когдаитераторвозвращаетnil.(Вследующемразделемыувидим,чтоэтодалеконевсе,чтоделаетобщийfor.)

Какболеепродвинутыйпример,листинг7.1показываетитератордляперебора всех слов из текущего входного файла. Для такого переборанамнужнодвазначения:содержимоетекущейстроки(переменнаяline)и где мы находимся внутри этой строки (переменная pos). С этимиданными мы можем всегда сгенерировать следующее слово. Основнаячастьитерирующейфункции—этовызовstring.find.Этотвызовищетслово в текущей строке, начиная с текущей позиции. Он описывает«слово», используя образец '%w+', которому удовлетворяет один илиболеебуквенно-цифровыхсимволов.Еслиэтотвызовнаходитслово,тофункцияобновляеттекущуюпозициюнапервыйсимволпослесловаивозвращает это слово (Примечание: Функция string.sub извлекаетподстроку из line между заданными позициями; мы подробнорассмотримеевразделе21.2).Иначеитераторчитаетследующуюстрокуи повторяет поиск. Если строк больше нет, он возвращает nil, чтобысообщитьобокончанииитерации.

Несмотрянаегосложность,использованиеallwordsкрайнепросто:forwordinallwords()do

Page 91: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(word)

end

Это типичная ситуация с итераторами: их может быть не так легконаписать, зато их легко использовать.Серьезной проблемы в этом нет;гораздо чаще конечные пользователи, программирующие на Lua, неопределяют итераторы, а лишь используют те, что предоставленыприложением.

Листинг 7.1. Итератор для обхода всех слов из входногофайла

functionallwords()

localline=io.read()--текущаястрока

localpos=1--текущаяпозициявстроке

returnfunction()--итерирующаяфункция

whilelinedo--повторяет,покаестьстроки

locals,e=string.find(line,"%w+",pos)

ifsthen--словонайдено?

pos=e+1--следующаяпозицияпослеэтогослова

returnstring.sub(line,s,e)--возвращаетэтослово

else

line=io.read()--словоненайдено;пробуетследующую

строку

pos=1--перезапускспервойпозиции

end

end

returnnil--строкбольшенет;конецобхода

end

end

7.2.Семантикаобщегоfor

Единственным недостатком ранее рассмотренных итераторовявляется то, что нам необходимо создавать новое замыкание дляинициализациикаждогоновогоцикла.Длябольшинстваслучаевэтонеявляется проблемой. Например, в случае итератора allwords ценасоздания одного единственного замыканиянесравнима с ценой чтенияцелого файла. Однако, в некоторых ситуациях эта лишняя нагрузкаможетбытьнеприемлемой.Втакихслучаяхмыможемиспользоватьсамобщий for для хранения состояния итерации. В данном разделе мыувидим,какиевозможностипохранениюсостоянияпредлагаетобщийfor.

Мы видели, что общий for во время выполнения цикла хранит

Page 92: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

итерирующую функцию внутри себя. На самом деле он хранит тризначения: итерирующую функцию, инвариантное состояние(неизменяющееся) и управляющую переменную. Теперь давайтеобратимсякдеталям.

Синтаксисобщегоforследующий:

for<список_переменных>in<список_выражений>do<тело>end

Здесь список_переменных — это список из одного или несколькихимен переменных, разделенных запятыми, a список_выражений — этосписок из одного или нескольких выражений, также разделенныхзапятыми.Частосписоквыраженийсостоитизединственногоэлемента— вызова фабрики итераторов. В следующем коде, например, списокпеременных—этоk,v, а списоквыраженийсостоитиз единственногоэлементаpairs(t):

fork,vinpairs(t)doprint(k,v)end

Частосписокпеременныхтожесостоитвсегоизоднойпеременной,каквследующемцикле:

forlineinio.lines()do

io.write(line,"\n")

end

Мыназываемпервуюпеременнуюв списке управляющей переменной. Втечение всегоцикла ее значениеникогдане равноnil, поскольку когдаонастановитсяравнойnil,циклзавершается.

Первое,чтоделаетцикл for,—вычисляетвыраженияпосле in. Этивыражениядолжныдатьтризначения,которыехранитfor:итерирующаяфункция, инвариантное состояние и начальное значение управляющейпеременной.Какивомножественномприсваивании,толькопоследний(илиединственный)элементспискаможетдатьболееодногозначения;число этих значений приводится к трем, лишние значенияотбрасываются, вместо недостающих добавляются nil. (Когда мыиспользуем простые итераторы, фабрика возвращает толькоитерирующую функцию, поэтому инвариантное состояние иуправляющаяпеременнаяполучаютзначениеnil.)

После этого шага инициализации for вызывает итерирующуюфункцию с двумя аргументами: инвариантным состоянием и

Page 93: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

управляющей переменной. (С точки зрения конструкции for, этоинвариантное состояние вообще не имеет никакого смысла. Операторforлишьпередаетзначениесостоянияизшагаинициализацииввызовыитерирующейфункции.)Затем for присваивает значения, возвращенныеитерирующей функцией, переменным, объявленным в его спискепеременных. Если первое возвращенное значение (присваиваемоеуправляющей переменной) равно nil, то цикл завершается. Иначе forвыполняет свое тело и вновь вызывает итерирующую функцию,повторяяпроцесс.

Точнееговоря,конструкциявида

forvar_1,...,var_nin<список_выражений>do<блок>end

эквивалентаследующемукоду:do

local_f,_s,_var=<список_выражений>whiletruedo

localvar_1,...,var_n=_f(_s,_var)

_var=var_1

if_var==nilthenbreakend

<блок>end

end

Поэтомуеслинашаитерирующаяфункция—f,неизменяемоесостояние— s, а начальное состояние для управляющей переменной — a0, тоуправляющаяпеременнаябудетпробегатьследующиезначенияa1=f(s,a0),a2=f(s,a1)ит.д.,дотехпор,покааiнестанетравнойnil.Еслиуforесть другие переменные, то они просто получат дополнительныезначения,возвращаемыеприкаждомвызовеf.

7.3.Итераторыбезсостояния

Как следует из названия, итератор без состояния — это итератор,который не хранит в себе какое-либо состояние. Поэтому мы можемиспользоватьодини тотжеитераторбез состояниявомногихциклах,избегаятемсамымплатызасозданиеновыхзамыканий.

Как мы только что видели, оператор for вызывает своюитерирующуюфункциюсдвумяаргументами:инвариантноеостояниеиуправляющая переменная. Итератор без состояния генерирует

Page 94: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

следующий элемент цикла, используя только эти два значения.Типичным примером этого вида итератора является ipairs, которыйперебираетвсеэлементымассива:

a={"one","two","three"}

fori,vinipairs(a)do

print(i,v)

end

Состояние этойитерации—это таблица, которуюмыперебираем (т.е.инвариантное состояние, котороенеменяетсянапротяжениицикла), итекущий индекс (управляющая переменная). И ipairs (фабрика), иитератор очень просты; мы могли бы написать их на Lua следующимобразом:

localfunctioniter(a,i)

i=i+1

localv=a[i]

ifvthen

returni,v

end

end

functionipairs(a)

returniter,a,0

end

КогдаLuaвызываетipairs(а)вциклеfor,онаполучаеттризначения:функцию iter как итератор, а как инвариантное состояние и ноль какначальное значение для управляющей переменной. Затем Lua вызываетiter(а,0), что дает 1,а[1] (если только а[1] уже не nil). На второйитерации вызывается iter(а,1), что дает 2,а[2], и т. д., до первогоэлемента,равногоnil.

Функция pairs, которая перебирает все элементы таблицы,аналогична, за исключением того, что итерирующая функция — этофункцияnext,котораявLuaявляетсяпримитивной:

functionpairs(t)

returnnext,t,nil

end

Вызов next(t,k), где k— это ключ таблицы t, возвращает следующийключ в таблице в произвольном порядке, а также связанное с этимключом значение как второе возвращаемое значение.Вызов next(t,nil)возвращаетпервуюпару.Когдапарбольшенет,nextвозвращаетnil.

Page 95: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Некоторые предпочитают использовать next напрямую, избегаявызоваpairs:

fork,vinnext,tdo

<телоцикла>end

Вспомним,чтоforприводитсвойсписоквыраженийктремрезультатам,чтобыполучитьnext,tиnil;этоименното,чтополучаетсяпривызовеpairs.

Итератор для обхода связанного списка является еще одниминтереснымпримеромитераторабезсостояния.(Какмыужеупомянули,связанныеспискинечастовстречаютсявLua,ноиногдаонинамнужны.)

localfunctiongetnext(list,node)

ifnotnodethen

returnlist

else

returnnode.next

end

end

functiontraverse(list)

returngetnext,list,nil

end

Суть этого приема состоит в использовании основного узла списка вкачестве инвариантного состояния (второе значение, возвращаемоеtraverse) и текущего узла в качестве управляющей переменной. Припервом вызове итерирующей функции getnext переменная node будетравна nil, и поэтому функция вернет list как первый узел. Впоследующийвызовахnodeнебудетравнаnil,ипоэтомуитератор,какиожидалось, вернет node.next. Как обычно, использование этогоитераторакрайнепросто:

list=nil

forlineinio.lines()do

list={val=line,next=list}

end

fornodeintraverse(list)do

print(node.val)

end

7.4.Итераторысосложнымсостоянием

Page 96: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Часто итератору требуется хранить больший объем состояния, чемпомещается в единственное инвариантное состояние и управляющуюпеременную.Простейшимрешениемявляетсяиспользованиезамыканий.Альтернативнымрешениембудетзапаковатьвсе,чтонужноитератору,втаблицу и использовать эту таблицу как инвариантное состояние дляитерации. С помощью таблицы итератор можем хранить столькоданных,сколькоемупотребуетсянапротяжениицикла.Крометого,вовремяитерациионможетменятьэтиданные.Хотясостояние—этовсевремя одна и та же таблица (поэтому оно инвариантное), содержимоетаблицы может меняться на протяжении цикла. Поскольку такиеитераторы хранят все свои данные в состоянии, обычно ониигнорируют второй аргумент, предоставляемый общим циклом for(итерационнаяпеременная).

Вкачествепримератакогоподходамыперепишемитераторallwords,который обходит все слова текущего входного файла. На этот раз мыбудемхранитьегосостояниевтаблицесдвумяполями:lineиpos.

Функция,котораяначинаетитерацию,довольнопроста.Онадолжнавернутьитерирующуюфункциюиначальноесостояние:

localiterator--будетопределенапозже

functionallwords()

localstate={line=io.read(),pos=1}

returniterator,state

end

Основнуюработувыполняетфункцияiterator:functioniterator(state)

whilestate.linedo--повторяет,покаестьстроки

--ищетследующееслово

locals,e=string.find(state.line,"%w+",state.pos)

ifsthen--словонайдено?

--обновляетследующуюпозицию(послеэтогослова)

state.pos=e+1

returnstring.sub(state.line,s,e)

else--словоненайдено

state.line=io.read()--пробуетследующуюстроку...

state.pos=1--...начинаяспервойпозиции

end

end

returnnil--строкбольшенет:конеццикла

end

Прилюбойвозможности вам следуетпытатьсянаписатьитераторыбезсостояния,такие,чтохранятвсесвоесостояниевпеременныхцикла

Page 97: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

for.Снимивынесоздаетеновыхобъектов,когданачинаетецикл.Еслидля вашей итерации эта модель не подходит, то вам следуетпопробовать замыкания. Помимо большей изящности, замыканиеобычно более эффективно, чем итератор с применением таблиц: во-первых,дешевлесоздать замыкание,чемтаблицу;во-вторых,доступкнелокальнымпеременнымбыстрее,чемдоступкполямтаблицы.Позжемы увидим еще один способ писать итераторы, основанный насопрограммах.Этосамоеэффективноерешение,хотяиболеезатратное.

7.5.Подлинныеитераторы

Термин «итератор» несколько неточен, поскольку итерациювыполняют не наши итераторы, а цикл for. Итераторы лишьпредоставляют последовательные значения для итерации. Пожалуй,более удачным термином был бы «генератор», но термин «итератор»ужеполучилширокоераспространениевдругихязыках,такихкакJava.

Темнеменее,существуетдругойспособпостроенияитераторов,прикотором итераторы действительно выполняют итерацию. Когда мыиспользуем такие итераторы, мы не пишем цикл; вместо этого мыпросто вызываем итератор с аргументом, описывающим, что итератордолжен делать на каждой итерации. Точнее итератор получает вкачестве аргумента функцию, которую он вызывает внутри своегоцикла.

В качестве конкретного примера давайте еще раз перепишемитераторallwords,придерживаясьэтогостиля:

functionallwords(f)

forlineinio.lines()do

forwordinstring.gmatch(line,"%w+")do

f(word)--вызываетфункцию

end

end

end

Для использования этого итератора мы просто должны предоставитьтелоциклакакфункцию.Еслинамнужновсеголишьнапечататькаждоеслово,томыпростоиспользуемprint:

allwords(print)

Зачастую в качестве тела цикла используется анонимная функция.Например,следующийфрагменткодасчитает,сколькоразслово«hello»

Page 98: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

встречаетсявовходномфайле:localcount=0

allwords(function(w)

ifw=="hello"thencount=count+1end

end)

print(count)

Та же самая задача, записанная в стиле предыдущего итератора, несильноотличается:

localcount=0

forwinallwords()do

ifw=="hello"thencount=count+1end

end

print(count)

Подобные итераторы были популярны в более старых версиях Lua,когда в языке не было оператора for. Какое отношение они имеют китераторамвстилегенераторов?Уобоихстилейпримерноодинаковыезатраты: один вызов функции на итерацию. С одной стороны, легчеписать итератор с использованием подлинных итераторов (хотя мыможем получить эту же легкость при помощи сопрограмм). С другойстороны,стильгенераторовболеегибкий.Во-первых,онпозволяетдвеиболеепараллельныхитерации.(Например,рассмотримслучайпереборасразу двух файлов, сравнивая их пословно.) Во-вторых, он позволяетиспользование break и return внутри тела итератора. С подлиннымиитераторами return возвращает из анонимной функции, но не изфункции,совершающейитерацию.Восновном,яобычнопредпочитаюгенераторы.

Упражнения

Упражнение 7.1. Напишите итератор fromto так, чтобы следующиедвациклаоказалисьэквивалентными:

foriinfromto(n,m)

<тело>end

fori=n,m

<тело>end

Можетеливыреализоватьегоприпомощиитераторабезсостояния?

Page 99: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнение7.2.Добавьтепараметр,отвечающийзашаг,китераторуиз предыдущего упражнения. Можете ли вы по-прежнему реализоватьегокакитераторбезсостояния?

Упражнение 7.3. Напишите итератор uniquewords, которыйвозвращает все слова из заданного файла без повторений. (Подсказка:начните с кода allwords из листинга 7.1; используйте таблицу, чтобыхранитьвсеслова,которыевыужевернули.)

Упражнение 7.4. Напишите итератор, который возвращает всенепустые подстроки заданной строки. (Вам понадобится функцияstring.sub.)

Page 100: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА8

Компиляция,выполнениеиошибки

Хотя мы называем Lua интерпретируемым языком, Lua всегдапредкомпилирует исходный код в промежуточную форму перед еговыполнением.(Вэтомнетничегострашного:многиеинтерпретируемыеязыкиделают тоже самое.)Наличие этапа компиляцииможетказатьсянеуместным в интерпретируемом языке вроде Lua. Однако,отличительнымпризнакоминтерпретируемыхязыковявляетсянето,чтоони не компилируются, а то, что в них возможно (и легко) выполнятькод,генерируемыйналету.Можносказать,чтоналичиефункциивродеdofile — это то, что позволяет Lua называться интерпретируемымязыком.

8.1.Компиляция

Ранеемыввелиdofile как своего родапримитивнуюоперациюдлявыполнения кусков кода Lua, но dofile на самом деле являетсявспомогательной функцией: всю тяжелую работу выполняет loadfile.Какиdofile,loadfileзагружаеткусоккодаLuaизфайла,ноприэтомневыполняет его. Вместо этого он только компилирует этот кусок ивозвращает скомпилированный кусок как функцию. Более того, вотличиеотdofile,loadfileневызываетошибки,анаоборотвозвращаетих коды, чтобы мы могли разобраться с ошибкой. Мы могли быопределитьdofileследующимобразом:

functiondofile(filename)

localf=assert(loadfile(filename))

returnf()

end

Обратите внимание на использование assert для вызова ошибки, еслиloadfileдастсбой.

Дляпростыхзадачdofile удобна,посколькувыполняетвсюработузаодинвызов.Однакоloadfileболеегибкая.Вслучаеошибкиloadfileвозвращает nil и сообщение об ошибке, что позволяет намобработатьошибку более подходящими для нас способами. Более того, если нам

Page 101: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

нужно выполнить файл несколько раз, то мы можем один раз вызватьloadfile и несколько раз вызвать возвращенную им функцию. Этотподходгораздодешевле,чемнесколькоразвызыватьdofile,посколькуфайлкомпилируетсялишьодинраз.

Функцияloadпохожанаloadfile,заисключениемтого,чтоонабереткусок кода не из файла, а из строки (Примечание: В Lua 5.1 функцияloadstring выполняет роль load). Например, рассмотрим следующуюстроку:

f=load("i=i+1")

Послеэтогокодаfстанетфункцией,котораяпривызовевыполняетi=i+1:

i=0

f();print(i)-->1

f();print(i)-->2

Функция load довольно мощная, и мы должны использовать ее состорожностью. Это также затратная функция (по сравнению снекоторыми альтернативами), которая может привести к непонятномукоду. Перед ее использованием, убедитесь, что в вашем распоряжениинетболеепростогоспособарешитьзадачу.

Если хотите по-быстрому воспользоваться dostring (т.е. загрузить ивыполнить кусок), можете непосредственно использовать результатload:

load(s)()

Однако,еслиестьхотябыоднасинтаксическаяошибка,тоloadвернетnilиокончательнымсообщениемобошибкебудетчто-товроде"attemptto call a nil value". Для более понятных сообщений об ошибкахиспользуйтеassert:

assert(load(s))()

Обычнонетникакогосмыслаприменятьфункциюloadнастроковомлитерале.Например,следующиедвестрокипримерноэквиваленты:

f=load("i=i+1")

f=function()i=i+1end

Тем не менее, вторая строка кода гораздо быстрее, поскольку Lua

Page 102: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

скомпилирует функцию вместе с окружающим ее куском. В первойстрокевызовloadвключаетотдельнуюкомпиляцию.

Поскольку load не компилирует с учетом лексической областидействия, то те две строки из предыдущего примера могут быть несовсем эквивалентны. Чтобы увидеть разницу, давайте слегка изменимпример:

i=32

locali=0

f=load("i=i+1;print(i)")

g=function()i=i+1;print(i)end

f()-->33

g()-->1

Функция g работает с локальной i, как и ожидалось, но f работает сглобальной i, поскольку load всегда компилирует свои куски вглобальномокружении.

Наиболее типичным использованием load является выполнениевнешнего кода, то есть фрагментов кода, поступающих извне вашейпрограммы. Например, вам может потребоваться построить графикфункции, заданнойпользователем;пользовательвводиткодфункции,авы затем используете load, чтобы выполнить его. Обратите внимание,что load ожидает получить кусок, то есть операторы. Если вы хотитевычислить выражение, то вы можете поставить перед выражениемreturn, что даст вам оператор, возвращающий значение заданноговыражения.Посмотритенапример:

print"enteryourexpression:"

locall=io.read()

localfunc=assert(load("return"..l))

print("thevalueofyourexpressionis"..func())

Поскольку функция, возвращенная load, является обычной, вы можетевызватьеенесколькораз:

print"enterfunctiontobeplotted(withvariable'x'):"

locall=io.read()

localf=assert(load("return"..l))

fori=1,20do

x=i--глобальная'x'(чтобыбытьвидимойизэтогокускакода)

print(string.rep("*",f()))

end

(Функцияstring.repповторяетстрокузаданноечислораз.)Мы также можем вызвать функцию load, передав ей в качестве

Page 103: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

аргумента считывающую функцию (reader function). Считывающаяфункция может возвращать кусок кода частями; load последовательновызываетеедотех,покаонаневернетnil,обозначающийконецкуска.Вкачествепримераследующийвызовэквивалентенloadfile:

f=load(io.lines(filename,"*L"))

Какмыподробнорассмотримв главе22, вызовio.lines(filename,"*L")возвращает функцию, которая при каждом своем вызове возвращаетследующуюстрокуиззаданногофайла(Примечание:Опциидляio.linesпоявилисьтольковLua5.2).Такимобразом,loadбудетчитатькусокизфайла строка за строкой. Следующий вариант похож, но болееэффективен:

f=load(io.lines(filename,1024))

Здесь итератор, возвращенный io.lines, читает файл блоками по 1024байта.

Lua рассматривает каждый независимый кусок как тело анонимнойфункции с переменным числом аргументов. Например, load("а = 1")

возвращаетаналогследующеговыражения:function(...)a=1end

Как и любые другие функции, куски могут объявлять локальныепеременные:

f=load("locala=10;print(a+20)")

f()-->30

Используя эти возможности, мы можем переписать наш пример спостроением графика так, чтобы избежать применения глобальнойпеременнойх:

print"enterfunctiontobeplotted(withvariable'x'):"

locall=io.read()

localf=assert(load("localx=...;return"..l))

fori=1,20do

print(string.rep("*",f(i)))

end

Мыставимобъявление"localx=..."вначалокуска,чтобыопределитьх как локальную переменную. Затем мы вызываем f с аргументом i,который становится значением выражения с переменным числомаргументов(...).

Page 104: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Функции загрузки кусков никогда не вызывают ошибки. В случаеошибкилюбогородаонивозвращаютnilисообщениеобошибке:

print(load("ii"))

-->nil[string"ii"]:1:'='expectednear'i'

Более того, у этих функций нет никакого побочного эффекта. Онитолькокомпилируюткусоквовнутреннеепредставлениеивозвращаютрезультат как анонимную функцию. Распространенная ошибка —полагать, что загрузка куска определяет функции. В Lua определенияфункций являются присваиваниями; поэтому они происходят во времявыполнения,аневовремякомпиляции.Например,допустим,чтоунасестьфайлfоо.luaнаподобиеследующего:

--файл'foo.lua'

functionfoo(x)

print(x)

end

Затеммывыполняемкомандуf=loadfile("foo.lua")

Послеэтойкомандыfooскомпилирована,ноещенеопределена.Чтобыопределитьее,мыдолжнывыполнитьэтоткусок:

print(foo)-->nil

f()--определяет'foo'

foo("ok")-->ok

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

8.2.Предкомпилированныйкод

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

Простейшим способом получения предкомпилированного файла(бинарного куска на жаргоне Lua) является использование программы

Page 105: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

luac, которая входит в стандартную поставку. Например, следующийвызов создает новый файл prog.1с с предкомпилированной версиейфайлаprog.lua:

$luac-oprog.lcprog.lua

ИнтерпретаторможетвыполнитьэтотновыйфайлпрямокакобычныйкодLua,работаяснимточнотакже,какисисходнымфайлом:

$luaprog.lc

Lua принимает предкомпилированный код практически везде, где ондопускает исходный код. В частности, loadfile и load принимаютпредкомпилированныйкод.

Мы можем написать упрощенную версию luac непосредственно наLua:

p=loadfile(arg[1])

f=io.open(arg[2],"wb")

f:write(string.dump(p))

f:close()

Основнаяфункцияздесь—этоstring.dump:онаполучаетфункциюLuaивозвращает ее предкомпилированный код как строку, правильнооформленнуюдляееобратнойзагрузкивLua.

Программа luac предлагает некоторые другие интересные опции. Вчастности, опция -l печатает список кодов операций, которыекомпиляторгенерируетдляданногокуска.Вкачествепримералистинг8.1 содержит вывод программы luac с опцией -l для следующегооднострочногофайла:

a=x+y-z

(МынебудемобсуждатьвнутреннееустройствоLuaвэтойкниге;есливас интересуют подробности об этих кодах операций, то поиск вИнтернетепофразе''luaopcode"должендатьвамподходящийматериал.)

Листинг8.1.Примервыводаluac-lmainstdin:0,0(7instructions,28bytesat0x988cb30)

0+params,2slots,0upvalues,0locals,4constants,0functions

1[1]GETGLOBAL0-2;x

2[1]GETGLOBAL1-3;y

3[1]ADD001

4[1]GETGLOBAL1-4;z

5[1]SUB001

Page 106: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

6[1]SETGLOBAL0-1;a

7[1]RETURN01

Код в предкомпилированной форме не всегда меньше исходногокода, но он загружается быстрее. Еще одним преимуществом являетсято, что это дает вам защиту от случайных изменений в исходниках.Однако,вотличиеотисходногокода,поврежденныйзлоумышленникомбинарныйкодможетпривестикпадениюинтерпретатораLuaилидажевыполнить предоставленный пользователем машинный код. Привыполнении обычного кода вам не о чем беспокоиться. Однако, вамследуетизбегатьвыполненияненадежногокодавпредкомпилированнойформе.Уфункцииloadестьопциякакраздляэтойзадачи.

Кроме обязательного первого аргумента, у load есть еще тринеобязательных. Вторым аргументом является имя куска, котороеиспользуетсятольковсообщенияхобошибках.Четвертыйаргумент—этоокружение,котороемыобсудимвглаве14.Третийаргумент—этоименно то, что нас здесь интересует; он управляет тем, какие видыкусковмогутбытьзагружены.Приналичииэтотаргументдолженбытьстрокой: строка "t" позволяет загружать лишь текстовые (обычные)куски,"b" позволяет загружать лишьбинарные (предкомпилированные)куски,а"bt",значениепоумолчанию,разрешаетобаформата.

8.3.КодС

В отличие от кода, написанного на Lua, код С должен бытьскомпонован (связан) с приложением перед его использованием. Внекоторыхпопулярныхоперационныхсистемахнаиболеелекий способсоздания этой связи заключается вприменении средствадинамическойкомпоновки.Однако,данноесредствонеявляетсячастьюспецификацииANSIС;поэтомупереносимогоспособаегореализациинесуществует.

Обычно Lua не содержит средства, которые не могут бытьреализованы в ANSI С. Однако, с динамической компоновкой инаяситуация.Мыможемрассматриватьеекакосновувсехдругихсредств:имеяее,мыспособныдинамическиподгружатьлюбоедругоесредство,которогонетвLua.ПоэтомувданномособомслучаеLuaотказываетсяот правил переносимости и реализует средство динамическойкомпоновкидлярядаплатформ.Стандартнаяреализацияпредлагаетегоподдержку для Windows, Mac OS X, Linux, FreeBSD, Solaris ибольшинства других реализаций UNIX. Перенос данного средства на

Page 107: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

другие платформы должен быть не сложным; проверьте вашдистрибутив. (Для этого выполните print(package.loadlib("a","b")) изприглашениявводаLuaипосмотритенарезультат.Еслионжалуетсянанесуществующий файл, то у вас есть средство динамическойкомпоновки.Впротивномслучаесообщениеобошибкедолжноуказать,чтоданноесредствонеподдерживаетсяилинеустановлено.)

Lua предоставляет все возможности динамической компоновкипосредством единственной функцию под названием package.lodlib. Унее есть два строковых аргумента: полный путь к библиотеке и имяфункцииизэтойбиблиотеки.Поэтомуеетипичныйвызоввыглядиткакследующийфрагменткода:

localpath="/usr/local/lib/lua/5.1/socket.so"

localf=package.loadlib(path,"luaopen_socket")

Функция loadlib загружает заданную библиотеку и связывает ее с Lua.Однако, она не вызывает заданную функцию. Вместо этого онавозвращаетфункциюСкакфункциюLua.Вслучаеошибкипризагрузкебиблиотеки или нахождении инициализирующей функции loadlib

возвращаетnilисообщениеобошибке.Функция loadlib является очень низкоуровневой. Мы должны

предоставить полный путь к библиотеке и правильное имя функции(включая символы подчеркивания в начале, время от временидобавляемые компилятором). Чаще всего мы загружаем библиотеки Спосредством require. Эта функция ищет библиотеку и используетloadlib, чтобы загрузить для нее инициализирующую функцию. Привызове этаинициализирующаяфункциястроитивозвращаеттаблицусфункциямииз этой библиотекиподобно тому, как это делает обычнаябиблиотека Lua. Мы обсудим require в разделе 15.1 и подробнеерассмотримбиблиотекиСвразделе27.3.

8.4.Ошибки

Errarehumanumest (Человеку свойственноошибаться).Поэтомумыдолжны обрабатывать ошибки лучшим из доступных нам способов.Поскольку Lua является расширяющим языком, часто встраиваемым вприложение,оннеможетпростоупастьилизавершитьработувслучаевозникновения ошибки. Вместо этого, каждый раз, когда возникаетошибка, Lua завершает текущий кусок и возвращает управление вприложение.

Page 108: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Любаянеожиданнаяситуация,скоторойсталкиваетсяLua,вызываетошибку. Ошибки возникают, когда вы (то есть ваша программа)пытаетесь сложить значения, которые не являются числами,индексировать не таблицу и т. п. (Вы можете изменить это поведениепри помощи метатаблиц, как мы увидим позже.) Вы также можетенапрямую вызвать ошибку при помощи вызова функции error ссообщением об ошибке в качестве аргумента. Обычно эта функцияявляется подходящим способом для оповещения об ошибках в вашемкоде:

print"enteranumber:"

n=io.read("*n")

ifnotnthenerror("invalidinput")end

Данная конструкция вызова error для некоторых условий настолькораспространена,чтодлянеевLuaестьвстроеннаяфункцияassert:

print"enteranumber:"

n=assert(io.read("*n"),"invalidinput")

Функция assert проверяет, действительно ли ее первый аргумент неложен, и просто возвращает этот аргумент; если аргумент ложен, тоassertвызываетошибку.Еевторойаргумент(сообщение)необязателен.Однако, имейте в виду, что assert— это обычная функция. В связи сэтимLuaвсегдавычисляетееаргументыпередвызовом.Поэтомуесливынапишитечто-товроде

n=io.read()

assert(tonumber(n),"invalidinput:"..n.."isnotanumber")

тоLuaвсегдавыполнитконкатенацию,дажекогдаnявляетсячислом.Втакихслучаяхразумнеебываетиспользоватьявнуюпроверку.

Когда функция обнаруживает непредвиденную ситуацию(исключение),ейдоступныдвеосновныелинииповедения:вернутькодошибки(обычноnil)иливызватьошибкупосредствомвызовафункцииerror. Не существует жестких правил для выбора между этими двумявариантами, но мы можем дать общую рекомендацию: исключение,котороелегкообходится,должновызыватьошибку;иначеонодолжновернутькодошибки.

Например, давайте рассмотрим функцию sin. Как она должна себявести при вызове на таблице? Предположим, она возвращает кодошибки. Если бы нам понадобилась проверка на ошибки, мы бынаписаличто-товроде

Page 109: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localres=math.sin(x)

ifnotresthen--ошибка?

<кодобработкиошибки>

Однако, мымогли бы легко проверить это исключение перед вызовомфункции:

ifnottonumber(x)then--xнеявляетсячислом?

<кодобработкиошибки>

Часто мы не проверяем ни аргумент, ни результат вызова sin; еслиаргумент не является числом, значит, вероятно, с нашей программойтворитсянечтонеладное.Вподобнойситуациипрекратитьвычисленияи вызвать ошибку— это простейший и наиболее практичный способобработкиданногоисключения.

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

localfile,msg

repeat

print"enterafilename:"

localname=io.read()

ifnotnamethenreturnend--вводаданныхнебыло

file,msg=io.open(name,"r")

ifnotfilethenprint(msg)end

untilfile

Если вы не хотите обрабатывать подобные ситуации, но по-прежнемухотите перестраховаться, то для защиты данной операции вы можетепростоиспользоватьassert:

file=assert(io.open(name,"r"))

ЭтотипичнаядляLuaидиома:еслиio.openдастсбой,тоassertвызоветошибку.

Page 110: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

file=assert(io.open("no-file","r"))

-->stdin:1:no-file:Nosuchfileordirectory

Обратитевнимание,каксообщениеобошибке,котороеявляетсявторымрезультатомio.open,оказываетсявторымаргументомдляassert.

8.5.Обработкаошибокиисключений

Длямногихприложенийвамненужновыполнятьникакойобработкиошибок в Lua; этим занимается прикладная программа.Вся работа Luaначинаетсясвызоваотприложения,котороеобычнопроситвыполнитькусок. При любой ошибке этот вызов возвращает код ошибки, чтобыприложение могло предпринять соответствующие действия. В случаеавтономного интерпретатора его главный цикл лишь печатаетсообщение об ошибке, а затем продолжает показывать приглашениевводаивыполнятькоманды.

Однако, если вам надо обрабатывать ошибки в Lua, вы должныиспользовать pcall («protected call» — защищенный вызов) дляинкапсуляциивашегокода.

Допустим, вы хотите выполнить фрагмент кода Lua и пойматьлюбуюошибку,вызваннуюприеговыполнении.Вашимпервымшагомбудетинкапсулировать этотфрагмент кода вфункцию; в большинствеслучаев для этого используются анонимные функции. Затем вывызываетеэтуфункциюпосредствомpcall:

localok,msg=pcall(function()

<какой-нибудькод>ifunexpected_conditionthenerror()end

<какой-нибудькод>print(a[i])--потенциальнаяошибка:'a'можетнебытьтаблицей

<какой-нибудькод>end)

ifokthen--привыполнениизащищенногокодаошибокнет

<обычныйкод>else--защищенныйкодвызвалошибку:примитесоответственные

меры

<кодобработкиошибки>end

Функция pcall вызывает свой первый аргумент в защищенномрежиме, чтобы перехватывать любые ошибки во время выполненияфункции. Если ошибок нет, то pcall возвращает true и все значения,

Page 111: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

возвращенныетемвызовом.Иначеонавозвращаетfalseисообщениеобошибке.

Несмотря на свое название, сообщение об ошибке не обязано бытьстрокой:pcallвернетлюбоезначениеLua,котороевыпередалиerror.

localstatus,err=pcall(function()error({code=121})end)

print(err.code)-->121

Этимеханизмыпредоставляютвсе,чтовамнеобходимодляобработкиисключений в Lua. Мы выбрасываем исключение при помощи error иперехватываем его при помощи pcall. Сообщение об ошибкеидентифицируетвидошибки.

8.6.Сообщенияобошибкахиобратныетрассировки

Хотя в качестве сообщения об ошибке мы можем использоватьзначение любого типа, обычно сообщения об ошибках— это строки,описывающие, что пошло не так. В случае возникновения внутреннейошибки (например, при попытке индексировать нетабличное значение)Lua генерирует сообщение об ошибке; иначе сообщением об ошибкестановится значение, переданное функции error. В тех случаях, когдасообщение об ошибке является строкой, Lua пытается добавитьнекоторуюинформациюотомместе,гдепроизошлаошибка:

localstatus,err=pcall(function()a="a"+1end)

print(err)

-->stdin:1:attempttoperformarithmeticonastringvalue

localstatus,err=pcall(function()error("myerror")end)

print(err)

-->stdin:1:myerror

Эта информация о месте ошибки содержит имя файла (в примере этоstdin)иномерегострокикода(впримереэто1).

У функции error есть второй дополнительный параметр, которыйзадает уровень, на котором она должна докладывать об ошибке; выиспользуете этот параметр, чтобы обвинить кого-то другого в случаеошибки. Скажем, вы написали функцию, чьей первой задачей являетсяпроверка,чтоонабылаправильновызвана:

functionfoo(str)

iftype(str)~="string"then

error("stringexpected")

end

Page 112: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

<обычныйкод>end

Затемкто-товызываетвашуфункциюснеправильнымаргументом:foo({x=1})

Еслиоставитьвсекакесть,Luaпокажетнавашуфункцию—ведьэтожеfooвызвалаerror,—аненанастоящеговиновника,которыйвызвалее с неправильным аргументом.Для исправления данной проблемы выинформируетеerror,чтоошибка,окоторойвыдокладываете,возниклана уровне 2 в иерархии вызовов (уровень 1 — это ваша собственнаяфункция):

functionfoo(str)

iftype(str)~="string"then

error("stringexpected",2)

end

<обычныйкод>end

Часто при возникновении ошибки мы хотим получить большеотладочной информации, а не только место возникновения. Какминимумнамнужнаобратнаятрассировка,показывающаяполныйстеквызовов,приведшихкошибке.Когдаpcallвозвращаетсвоесообщениеоб ошибке, она уничтожает часть стека (часть от нее до моментавозникновения ошибки). Следовательно, если нам требуется обратнаятрассировка,мыдолжныпостроить ее до возвратаизpcall. Для этогоLua предоставляет функцию xpcall. Кроме функции, которую нужновызвать, она получает второй аргумент — функцию обработкисообщений.ВслучаеошибкиLuaвызываетэтотобработчиксообщенийперед раскруткой стека, поэтому он может использовать отладочнуюбиблиотеку для получения любой дополнительной информации обошибке, которая ему потребуется. Двумя наиболее распространеннымиобработчиками ошибок являются debug.debug, предоставляющий вамприглашение ввода Lua, чтобы вы могли сами посмотреть, чтопроисходило в момент возникновения ошибки; и debug.traceback,который строит расширенное сообщение об ошибке с обратнойтрассировкой (Примечание: В главе 24 мы больше узнаем об этихфункциях,когдаобсудимотладочнуюбиблиотеку).Именнопоследнююфункциюиспользуетавтономныйинтерпретатордляпостроениясвоихсообщенийобошибках.

Page 113: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнения

Упражнение 8.1. Часто бывает удобно добавить какой-нибудьпрефиксккускукодаприегозагрузке.(Ранеемырассматривалипримервданной главе, гдемы ставилиreturn перед загружаемым выражением.)Напишите функцию loadwithprefix, которая работает как load, заисключением того, что она добавляет свой первый дополнительныйаргумент(строку)вкачествепрефиксадлязагружаемогокуска.

Какиоригинальнаяload,функцияloadwithprefix должнаприниматькуски,представленныекакстроками,такисчитывающимифункциями.Дажевслучае,когдаизначальныйкусокявляетсястрокой,loadwithprefixв действительности не должна конкатенировать префикс с куском.Вместо этого она должна вызвать load с соответствующейсчитывающей функцией, которая сперва возвратит префикс, и лишьзатемисходныйкусок.

Упражнение 8.2. Напишите функцию multiload, которая обобщаетloadwithprefix,получаясписоксчитывающихфункций,каквследующемпримере:

f=multiload("localx=10;",

io.lines("temp","*L"),

"print(x)")

Для приведенного выше примера multiload должна загрузить кусок,эквивалентный конкатенации строки "local..." с содержимым файлаtempистроки"print(х)".Какифункцияloadwithprefix изпредыдущегоупражнения, данная функция в действительности не должна ничегоконкатенировать.

Упражнение 8.3. Функция string.rep в листинге 8.2 используеталгоритм двоичного умножения для конкатенации n копий заданнойстроки s. Для любого фиксированного n мы можем создатьспециализированную версию string.rep, развертывая цикл впоследовательностькомандr=r..sиs=s..s.Вкачествепримерадляn=5разверткадастнамследующуюфункцию:

functionstringrep_5(s)

localr=""

r=r..s

s=s..s

s=s..s

r=r..s

returnr

Page 114: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

end

Напишите функцию, которая для заданного n возвращает функциюstringrep_n. Вместо использования замыкания ваша функция должнапостроитьтекстфункцииLuaссоответствующейпоследовательностьюкоманд (r=r..s и s=s..s) и затем использовать load для полученияитоговойфункции.Сравнитебыстродействиеобщейфункцииstring.rep(илизамыканиясееиспользованием)ивашимизаказнымифункциями.

Листинг8.2.Повторениестрокfunctionstringrep(s,n)

localr=""

ifn>0then

whilen>1do

ifn%2~=0thenr=r..send

s=s..s

n=math.floor(n/2)

end

r=r..s

end

returnr

end

Упражнение 8.4. Можете ли вы найти такое значение для f, чтовыражение pcall(pcall,f) вернет false в качестве своего первогорезультата?

Page 115: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА9

Сопрограммы

Сопрограмма(coroutine)похожананить(всмыслемногонитевости):это поток выполнения со своим стеком, своими локальнымипеременными и своим указателем команд; но он разделяет глобальныепеременныеипочтивсеостальноесдругимисопрограммами.Основноеотличиемеждунитямиисопрограммами—этото,чтоконцептуально(или буквально, в случае многопроцессорной машины) программа снитями выполняет несколько нитей параллельно. Сопрограммы, сдругой стороны, работают совместно: в любой момент временипрограмма с сопрограммами выполняет только одну из своихсопрограмм, и эта выполняемая сопрограмма приостанавливает своевыполнение,толькокогдаееявнопопросятприостановиться.

Сопрограмма—этооченьмощнаяконцепция.Ипоэтомуиногдаеетрудно применять. Не волнуйтесь, если вы не поймете некоторыепримеры из этой главы при первом чтении. Вы можете дочитать доконца книгии вернуться сюдапозже.Но, пожалуйста, вернитесь; вашевремябудетпотраченоненапрасно.

9.1.Основысопрограмм

Lua пакует все связанные с сопрограммами функции в таблицуcoroutine. Функция create создает новые сопрограммы. У нее естьединственный аргумент — функция с кодом, которую сопрограммабудет выполнять. Она возвращает значение типа thread, котороепредставляет из себя новую сопрограмму. Часто аргументом createявляетсяанонимнаяфункция,какпоказанониже:

co=coroutine.create(function()print("hi")end)

print(co)-->thread:0x8071d98

Сопрограмма может быть в одном из четырех состояний:приостановленное— suspended, выполняемое— running, завершенное— dead, обычное — normal. Мы можем проверить состояниесопрограммыприпомощифункцииstatus:

Page 116: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(coroutine.status(co))-->suspended

Когдамысоздаемсопрограмму,оназапускаетсявприостановленномсостоянии; сопрограмма не начинает автоматически выполнять своетело при создании. Функция coroutine.resume (пере)запускаетвыполнение сопрограммы,меняя ее состояние из приостановленного ввыполняемое:

coroutine.resume(co)-->hi

В этом первом примере тело сопрограммы просто печатает "hi" ипрекращает выполнение, оставляя сопрограмму в завершенномсостоянии,изкоторогоейуженевернуться:

print(coroutine.status(co))-->dead

До сих пор сопрограммы выглядели не более чем усложненнымспособ вызовафункций.Настоящая сила сопрограмм идет от функцииyield,котораяпозволяетвыполняемойсопрограммеприостановитьсвоевыполнение (иными словами, уступить управление), чтобы она моглабытьвозобновленапозже.Давайтерассмотримпростойпример:

co=coroutine.create(function()

fori=1,10do

print("co",i)

coroutine.yield()

end

end)

Теперь, когда мы возобновляем эту сопрограмму, она начинает своевыполнениеивыполняетсядопервогоyield:

coroutine.resume(co)-->co1

Если мы проверим ее состояние, то увидим, что данная сопрограммаприостановленаи,следовательно,можетбытьсновавозобновлена:

print(coroutine.status(co))-->suspended

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

coroutine.resume(co)-->co2

Page 117: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

coroutine.resume(co)-->co3

...

coroutine.resume(co)-->co10

coroutine.resume(co)--ничегонепечатает

Вовремяпоследнеговызоваresumeтелосопрограммызавершаетцикл,азатемвозвращаетуправлениебезпечатичего-либо.Еслимыпопытаемсявозобновитьееснова,тоresumeвернетfalseисообщениеобошибке:

print(coroutine.resume(co))

-->falsecannotresumedeadcoroutine

Обратите внимание, что resume выполняется в защищенном режиме.Поэтому, если внутри сопрограммы есть какие-либо ошибки, Lua небудет показывать сообщение об ошибке, а просто вернет управлениевызовуresume.

Когда одна сопрограмма возобновляет другую, она неприостанавливается; в конце концов, мы не можем ее возобновить.Однако, она и не выполняется, так как выполняемой является другаясопрограмма.Поэтомуеесобственноесостояниемыназываемобычным.

ПолезнымсредствомвLuaявляетсято,чтопараresume—yieldможетобмениваться данными.Первая resume, у которой нет соответственнойожидающейееyield,передаетсвоидополнительныеаргументыглавнойфункциисопрограммы:

co=coroutine.create(function(a,b,c)

print("co",a,b,c+2)

end)

coroutine.resume(co,1,2,3)-->co125

Вызовresumeвозвращаетпослеtrue,сообщающего,чтонетошибок,всеаргументы,переданныесоответственнойyield:

co=coroutine.create(function(a,b)

coroutine.yield(a+b,a-b)

end)

print(coroutine.resume(co,20,10))-->true3010

Аналогично yield возвращает все дополнительные аргументы,переданныевсоответственнойresume:

co=coroutine.create(function(x)

print("co1",x)

print("co2",coroutine.yield())

end)

coroutine.resume(co,"hi")-->co1hi

coroutine.resume(co,4,5)-->co245

Page 118: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Наконец, когда сопрограмма завершается, любые значения,возвращенные ее главной функцией, передаются соответственнойresume:

co=coroutine.create(function()

return6,7

end)

print(coroutine.resume(co))-->true67

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

Для тех, кто уже знает что-то о сопрограммах, важно прояснитьнекоторыепонятияперед тем, какпродолжить.Luaпредлагает то, чтоназываетсяасимметричнымисопрограммами.Этозначит,чтоунееестьодна функция для приостановки выполнения сопрограммы и другаяфункция для возобновления приостановленной сопрограммы.Некоторыедругиеязыкипредлагаютсимметричныесопрограммы,когдаесть лишь одна функция для передачи управления от однойсопрограммыкдругой.

Некоторые называют асимметричные сопрограммыполусопрограммами (не будучи симметричными, они не являютсяполноценными со-). Однако, другие используют этот же термин дляобозначения ограниченной реализации сопрограмм, где сопрограммаможет приостановить свое выполнение, только когда она не вызываетникакуюдругуюфункцию,тоестькогдаунеенетожидающихвызововвееуправляющемстеке.Другимисловами,уступитьуправлениеможеттолько главное тело такой полусопрограммы. ПримеромполусопрограммвэтомпониманииявляютсягенераторывPython.

В отличие от разницы между симметричными и асимметричнымисопрограммами, разница между сопрограммами и генераторами (впредставленииPython)оченьглубока;генераторыпростонедостаточномощные, чтобы реализовать некоторые интересные конструкции,которые мы можем писать с полноценными сопрограммами. Luaпредлагает полноценные асимметричные сопрограммы. Те, ктопредпочитают симметричные сопрограммы, могут реализовать их наосновеасимметричныхсредствLua.Этонесложнаязадача.(Фактическикаждая передача управления выполняет yield, за которым следуетresume.)

9.2.Каналыифильтры

Page 119: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Однимизнаиболеехрестоматийныхпримеров сопрограммявляетсяпроблема потребителя (producer) и производителя (consumer). Давайтепредставим, что у нас есть функция, которая постоянно производитзначения (например, читает их из файла), и другая функция, котораяпостоянно потребляет эти значения (например, пишет в другой файл).Обычноэтидвефункциивыглядятследующимобразом:

functionproducer()

whiletruedo

localx=io.read()--производитновоезначение

send(x)--отправляетегопотребителю

end

end

functionconsumer()

whiletruedo

localx=receive()--получаетзначениеотпроизводителя

io.write(x,"\n")--потребляетего

end

end

(Вэтойреализацииипроизводитель,ипотребительвыполняютсявечно.Но их легко изменить, чтобы они останавливались, когда больше нетданных для обработки.) Задача здесь заключается в том, чтобысопоставить send с receive. Это типичный образец проблемы «у когоглавныйцикл».Ипроизводитель,ипотребительактивны,уобоихестьсвои главные циклы, и каждый из них полагает, что другой являетсявызываемым сервисом. Для этого конкретного примера можно легкоизменить структуру одной из функций, развернув ее цикл и сделав еепассивным агентом. Однако, в других реальных случаях подобноеизменениеструктурыможетбытьдалеконетакимлегким.

Сопрограммы предоставляют идеальный механизм длясопоставления производителя с потребителем, поскольку пара resume—yield переворачивает типичное отношение между вызывающим ивызываемым.Когдасопрограммавызываетyield,онаневходитвновуюфункцию;вместоэтогоонавозвращаетуправлениеожидающемувызову(resume). Аналогично вызов resume не начинает новую функцию, авозвращает вызов yield. Это именно то, что нам нужно длясопоставления send с receive таким образом, чтобы каждый из нихдействовал так, будто главным является именно он, а второй являетсяподчиненным.Поэтомуreceive возобновляет производителя, чтобы онмогпроизвестиновоезначение,аsendпосредствомyieldвозвращаетэтозначениеобратнопотребителю:

Page 120: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionreceive()

localstatus,value=coroutine.resume(producer)

returnvalue

end

functionsend(x)

coroutine.yield(x)

end

Разумеется,теперьпроизводительдолженбытьсопрограммой:producer=coroutine.create(

function()

whiletruedo

localx=io.read()--производитновоезначение

send(x)

end

end)

При такой схеме программа начинает с вызова потребителя. Когдапотребителю нужен какой-то элемент, он возобновляет работупроизводителя, которыйвыполняетсядо техпор,покаунегонебудетэлемента для передачи потребителю, а затем останавливается, покапотребительсноваегоневозобновит.Такимобразом,мыполучаемто,что называется схемой, ориентированной на потребителя (consumer-driven).Другимвариантомбылобынаписатьпрограммусприменениемсхемы, ориентированной на производителя (producer-driven), гдепотребительявляетсясопрограммой.

Мы можем расширить эту схему при помощи фильтров, которыеявляются заданиями, находящимися между производителем ипотребителем и выполняющими своего рода преобразование данных.Фильтр — это производитель и потребитель в одно и то же время,поэтомуонвозобновляетпроизводителядляполученияновыхзначенийи посредством yield передает эти преобразованные значенияпотребителю, В качестве простого примера мы можем добавить кнашему предыдущему коду фильтр, который в начало каждой строкивставляет ее номер. Код приведен в листинге 9.1. Этот последнийкусочек просто создает нужные ему компоненты, соединяет их иначинаетвыполнениеитоговогопотребителя:

p=producer()

f=filter(p)

consumer(f)

Илиещелучше:

Page 121: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

consumer(filter(producer()))

Листинг9.1.Потребительипроизводительсфильтрамиfunctionreceive(prod)

localstatus,value=coroutine.resume(prod)

returnvalue

end

functionsend(x)

coroutine.yield(x)

end

functionproducer()

returncoroutine.create(function()

whiletruedo

localx=io.read()--производитновоезначение

send(x)

end

end)

end

functionfilter(prod)

returncoroutine.create(function()

forline=1,math.hugedo

localx=receive(prod)--получаетновоезначение

x=string.format("%5d%s",line,x)

send(x)--отправляетегопотребителю

end

end)

end

functionconsumer(prod)

whiletruedo

localx=receive(prod)--получаетновоезначение

io.write(x,"\n")--потребляетновоезначение

end

end

Еслипослепрочтенияпредыдущегопримеравыподумалиоканалах(pipe) вUNIX, то выне одиноки.В конце концов, сопрограммы—эторазновидность (невытесняющей) многонитевости. С каналами каждаязадача выполняется в отдельном процессе; с сопрограммами каждаязадача выполняется в отдельной сопрограмме. Каналы предоставляютбуфермеждупишущим(производителем)ичитающим(потребителем),поэтому возможна некоторая свобода в их относительных скоростях.Применительно к каналам это важно, поскольку цена переключениямеждупроцессамивысока.Ссопрограммамиценапереключениямежду

Page 122: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

задачами намного меньше (примерно равна вызову функции), поэтомупишущийичитающиймогутидтиногавногу.

9.3.Сопрограммыкакитераторы

Мы можем рассматривать итераторы на основе цикла в качествеконкретного примера схемы производитель-потребитель: итераторпроизводит элементы, потребляемые телом цикла. Поэтому вполнеестественным будет использовать сопрограммы для написанияитераторов. Действительно, сопрограммы являются эффективныминструментом для решения этой задачи. Опять же, ключевойособенностью является их способность вывернуть наизнанкуотношение между вызывающим и вызываемым. С этой особенностьюмыможемписатьитераторы,неволнуясьохранениисостояниямеждупоследовательнымивызовамиитератора.

Чтобы проиллюстрировать этот вариант использования, давайтенапишем итератор для перебора всех перестановок заданного массива.Написание подобного итератора напрямую не так легко, но несложнобудет написать рекурсивную функцию, которая генерирует все этиперестановки. Идея проста: по очереди помещать каждый элементмассива на последнюю позицию и рекурсивно генерировать всеперестановки оставшихся элементов. Код приведен в листинге 9.2.Чтобыонсработал,мыдолжныопределитьсоответствующуюфункциюprintResultивызватьpermgenсподходящимиаргументами:

functionprintResult(a)

fori=1,#ado

io.write(a[i],"")

end

io.write("\n")

end

permgen({1,2,3,4})

-->2341

-->3241

-->3421

...

-->2134

-->1234

Листинг9.2.Функциядляполучениявсехперестановокизпервыхnэлементовa

Page 123: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionpermgen(a,n)

n=nor#a--значение'n'поумолчанию—размер'a'

ifn<=1then--ничегонеизменилось?

printResult(a)

else

fori=1,ndo

--помещаетi-ыйэлементкакпоследний

a[n],a[i]=a[i],a[n]

--генерируетвсепреобразованияпрочихэлементов

permgen(a,n-1)

--восстанавливаетi-ыйэлемент

a[n],a[i]=a[i],a[n]

end

end

end

Кактолькогенераторготов,преобразоватьеговитератор—простаязадача.Во-первых,заменимprintResultнаyield:

functionpermgen(a,n)

n=nor#a

ifn<=1then

coroutine.yield(a)

else

<какпрежде>

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

functionpermutations(a)

localco=coroutine.create(function()permgen(a)end)

returnfunction()--итератор

localcode,res=coroutine.resume(co)

returnres

end

end

Имея в распоряжении такой механизм, перебрать все перестановкимассиваприпомощиоператораforнесоставиттруда:

forpinpermutations{"a","b","c"}do

printResult(p)

end

-->bca

-->cba

-->cab

-->acb

Page 124: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

-->bac

-->abc

Функция permutations использует типичную для Lua схему, котораяпакует вызовresume с соответственной сопрограммойвнутрифункции.Эта схема настолько распространена, что Lua предоставляет для нееособую функцию: coroutine.wrap. Как и create, wrap создает новуюсопрограмму.Вотличиеотcreate,wrapневозвращаетсамусопрограмму;вместо этого она возвращает функцию, которая при вызовевозобновляет эту сопрограмму. В отличие от исходной resume, она невозвращает код ошибки как свой первый результат; вместо этого принеобходимости она вызывает ошибку. Используя wrap, мы можемнаписатьpermutationsследующимобразом:

functionpermutations(a)

returncoroutine.wrap(function()permgen(a)end)

end

Обычно использовать coroutine.wrap проще, чем coroutine.create.Онадаетнамименното,чтонамнужноотсопрограммы:функциюдляее возобновления. Однако, она менее гибкая. Не существует способапроверить состояние сопрограммы, созданной при помощи wrap. Болеетого,мынеможемпроверятьнаошибкивовремявыполнения.

9.4.Невытесняющаямногонитевость

Как мы видели ранее, сопрограммы обеспечивают разновидностьсовместной многонитевости. Каждая сопрограмма эквивалентна нити.Пара yield—resume переключает управление с одной нити на другую.Однако, в отличие от обычной многонитевости, сопрограммы неявляютсявытесняющими.Покасопрограммавыполняется,онанеможетбытьостановленаизвне.Онапрерываетсвоевыполнение,толькокогдаявно запрашивает это (черезвызовyield).Длярядаприложенийэтонеявляетсяпроблемой,скореенаоборот.Программированиегораздопрощев отсутствие вытеснения. Вам не нужно беспокоиться об ошибкахсинхронизации, поскольку вся синхронизация среди нитей в вашейпрограмме явная. Вам лишь нужно убедиться в том, что сопрограммавызываетyieldвнекритическойобластикода.

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

Page 125: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

это недопустимое поведение, которое привело к тому, что многиепрограммисты не рассматривают сопрограммы как альтернативутрадиционноймногонитевости.Какмыздесьувидим,уэтойпроблемыестьинтересное(иочевидное,еслизаглянутьвпрошлое)решение.

Давайте рассмотрим типичную многонитевую задачу: мы хотимскачать несколько удаленных файлов по HTTP. Для скачиваниянескольких удаленных файлов сначала мы должны разобраться, какскачать один удаленный файл. В этом примере мы используемразработанную Диего Нехабом библиотеку LuaSocket. Для скачиванияфайла сперва нужно установить соединение с его сайтом, отправитьзапроснафайл,получитьэтотфайл(блоками)изакрытьсоединение.НаLua мы можем написать это следующим образом. Для начала мызагружаембиблиотекуLuaSocket:

localsocket=require"socket"

Затем мы определяем хост и файл, который хотим скачать. В этомпримере мы скачаем справочное руководство по HTML 3.2 с сайтаконсорциумаWorldWideWeb:

host="www.w3.org"

file="/TR/REC-html32.html"

Затем мы открываем TCP-соединение с портом 80 (стандартныйпортдляHTTP-соединений)данногосайта:

c=assert(socket.connect(host,80))

Эта операция возвращает объект соединения, который мы используемдляотправкизапросанаполучениефайла:

c:send("GET"..file.."HTTP/1.0\r\n\r\n")

Затем мы читаем файл блоками по 1 Кб, записывая каждый блок встандартныйвывод:

whiletruedo

locals,status,partial=c:receive(2^10)

io.write(sorpartial)

ifstatus=="closed"thenbreakend

end

Функция receive возвращает или строку, которую прочла, или nil вслучаеошибки; впоследнем случаеона такжевозвращает кодошибки(status) и что она прочла до ошибки (partial). Когда хост закрывает

Page 126: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

соединение, мы печатаем оставшиеся входные данные и прерываемциклаприемаданных.

Послескачиванияфайламызакрываемсоединение:c:close()

Теперь, когдамы знаем, как скачать одинфайл, давайте вернемся кпроблеме скачиваниянесколькихфайлов.Простейшимподходомбудетскачивать по одному файлу за раз. Однако, этот последовательныйподход,когдамыначинаемчитатьфайлтолькопослетого,какзакончимс предыдущим, слишком медленный. При чтении удаленного файлапрограмма проводит основную часть времени, ожидая прибытияданных. Более точно, она проводит большую часть времениблокированной в вызове receive. Поэтому программа можетвыполняться гораздо быстрее, если будет скачивать все файлыпараллельно. Тогда, когда у соединения нет доступных данных,программа может читать их из другого соединения. Понятно, чтосопрограммы предлагают удобный способ для организации этиходновременныхскачиваний.Мысоздаемновуюнитьдлякаждойзадачиприема данных. Когда у нити нет доступных данных, она уступаетуправлениепростомудиспетчеру,которыйвызываетдругуюнить.

Чтобыпереписатьпрограммусприменениемсопрограмм,дляначаланужно переписать предыдущий код для скачивания как функцию.Результат приведен в листинге 9.3. Поскольку нам не интересносодержимоеудаленногофайла,функцияподсчитываетипечатаетразмерфайла вместо записи файла в стандартный вывод. (С несколькиминитями,читающимисразунесколькофайлов,навыходеполучиласьбыполнаямешанина).

Листинг9.3.Коддляскачиваниявеб-страницыfunctiondownload(host,file)

localc=assert(socket.connect(host,80))

localcount=0--countsnumberofbytesread

c:send("GET"..file.."HTTP/1.0\r\n\r\n")

whiletruedo

locals,status=receive(c)

count=count+#s

ifstatus=="closed"thenbreakend

end

c:close()

print(file,count)

end

Page 127: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

functionreceive(connection)

locals,status,partial=connection:receive(2^10)

returnsorpartial,status

end

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

functionreceive(connection)

connection:settimeout(0)--неблокируетданные

locals,status,partial=connection:receive(2^10)

ifstatus=="timeout"then

coroutine.yield(connection)

end

returnsorpartial,status

end

Вызов settimeout(0) делает любую операцию над соединениемнеблокирующей.Когдастатусоперацииравен"timeout",этозначит,чтооперация вернула управление, не выполнив свою задачу.В этом случаенить уступает управление. Отличный от false аргумент, переданныйyield, сообщает диспетчеру, что данная нить все еще выполняет своюзадачу. Обратите внимание, что даже в случае статуса "timeout" впеременной partial все равно содержатся прочитанные ранее данные,котороевозвратилосоединение.

Листинг9.4.содержиткоддиспетчераинекоторыйдополнительныйкод. Таблица threads содержит список всех активных нитей длядиспетчера. Функция get следит за тем, чтобы каждая загрузкавыполнялась в отдельной нити. Сам диспетчер в основном являетсяциклом, который перебирает все нити, возобновляя их одну за другой.Также он должен удалять из списка те нити, которые уже завершилисвои задачи. Цикл останавливается, когда больше нет нитей длявыполнения.

Листинг9.4.Диспетчерthreads={}--списоквсехживыхнитей

functionget(host,file)

--создаетсопрограмму

Page 128: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localco=coroutine.create(function()

download(host,file)

end)

--вставляетеевсписок

table.insert(threads,co)

end

functiondispatch()

locali=1

whiletruedo

ifthreads[i]==nilthen--нитейбольшенет?

ifthreads[1]==nilthenbreakend--списокпуст?

i=1--перезапускаетцикл

end

localstatus,res=coroutine.resume(threads[i])

ifnotresthen--нитьвыполниласвоюзадачу?

table.remove(threads,i)

else

i=i+1--перейтикследующейнити

end

end

end

Наконец, главная программа создает требуемые нити и вызываетдиспетчер.Например,чтобызагрузитьчетыредокументас сайтаW3C,главнаяпрограммаможетвыглядеть,какпоказанониже:

host="www.w3.org"

get(host,"/TR/html401/html40.txt")

get(host,"/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")

get(host,"/TR/REC-html32.html")

get(host,"/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt")

dispatch()--главныйцикл

Haмоемкомпьютерескачиваниеэтихчетырехфайловсиспользованиемсопрограмм занимает 6 секунд. С последовательным скачиванием этотребуетвдвасполовинойразабольшевремени(15секунд).

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

Page 129: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Чтобы избежать подобного поведения, мы можем использоватьфункциюselect из библиотекиLuaSocket: она позволяет заблокироватьпрограмму, находящуюся в ожидании, пока изменится статус в группесокетов. Изменения в реализации незначительны: нам нужно изменитьтолькодиспетчер,какпоказановлистинге9.5.Вцикленовыйдиспетчерсобирает в таблице timedout соединения, у которых превышено времяожидания. (Помните, что receive передает подобные соединенияфункции yield, поэтому вызов resume их возвращает). Если у всехсоединенийпревышеновремяожидания,тодиспетчервызываетselect,чтобы ждать изменения статуса у одного из этих. Эта окончательнаяреализацияработаеттакжебыстро,какипредыдущаяссопрограммами.Более того, без активного ожидания она потребляет лишь немногимбольшепроцессорноговремени,чемпоследовательнаяреализация.

Листинг9.5.Диспетчер,использующийselectfunctiondispatch()

locali=1

localtimedout={}

whiletruedo

ifthreads[i]==nilthen--большенетнитей?

ifthreads[1]==nilthenbreakend

i=1--перезапускаетцикл

timedout={}

end

localstatus,res=coroutine.resume(threads[i])

ifnotresthen--нитьвыполниласвою

задачу?

table.remove(threads,i)

else--превышеновремяожидания

i=i+1

timedout[#timedout+1]=res

if#timedout==#threadsthen--всенитизаблокированы?

socket.select(timedout)

end

end

end

end

Упражнения

Упражнение 9.1. Используйте сопрограммы для преобразованияфункцииизупражнения5.4вгенератордлякомбинаций,которыйможетбытьиспользованследующимобразом:

Page 130: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

forcincombinations({"a","b","c"},2)do

printResult(c)

end

Упражнение9.2.Реализуйтеизапуститекодизпредыдущегораздела(невытесняющаямногонитевость).

Упражнение9.3.РеализуйтефункциюtransferнаLua.Еслиподуматьотом,чтовызовыresume—yeildаналогичнывызовуфункцииивозвратуиз нее, то эта функция будет как goto: она приостанавливаетвыполняемуюсопрограммуивозобновляетлюбуюдругуюсопрограмму,заданную в качестве аргумента. (Подсказка: используйте что-то вродеdispatchдляуправлениявашимисопрограммами.Тогдаtransferуступитуправление dispatch, сообщая о том, какую следующую сопрограммунужновыполнять,иdispatchвозобновитее.)

Page 131: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА10

Законченныепримеры

Чтобызавершитьвведениевязык,мыпокажемтрихотьипростых,но законченныхпримерапрограмм.Первыйпримеропроблемевосьмикоролев. Второй является программой частотности слов, котораяпечатаетсамыечастовстречающиесясловавтексте.Последнийпример— это реализация алгоритма цепи Маркова, описанная Керниганом иПайком (Kernighan & Pike) в их книге «The Practice of Programming»(Addison-Wesley,1999).

10.1.Задачаовосьмикоролевах

Нашпервыйпример—этооченьпростаяпрограмма,котораярешаетзадачуовосьмикоролевах:цельсостоитвразмещениивосьмикоролевнашахматной доске таким образом, чтобы ни одна из королев не могланапастьнадругую.

Первый шаг решения данной проблемы состоит в том, чтобыотметить, что у любого правильного решения должно быть ровно пооднойкоролевевкаждойстроке.Такимобразом,мыможемпредставитьрешенияприпомощипростогомассиваизвосьмичисел,поодномудлякаждой строки; каждое число сообщает нам, в каком столбцерасположена королева из каждой строки. Например, массив{3,7,2,1,8,6,5,4}обозначает,чтооднакоролеванаходитсявстроке1истолбце 3, другая — в строке 2 и столбце 7 и т. д. (Кстати, это неправильное решение; например, королева в строке 3 и столбце 2нападаетнакоролевув строке4и столбце1).Обратитевнимание, чтолюбоеправильноерешениедолжнобытьперестановкойцелыхчиселот1до8,таккакправильноерешениетакжедолжносодержатьпооднойкоролевевкаждомстолбце.

Полная программа приведена в листинге 10.1. Первой функциейявляетсяisplaceok,котораяпроверяет,чтозаданнаяпозициянадоскенеможетбытьатакованаранееразмещеннымикоролевами.Знаяотом,чтопо формулировке две королевы не могут находиться на одной строке,функцияisplaceokпроверяет,чтонетдругихкоролеввтомжестолбцеилитойжедиагонали,чтоуновойпозиции.

Page 132: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг10.1.Программа«восемькоролев»localN=8--boardsize

--проверяет,чтопозиция(n,c)находитсявнеатак

localfunctionisplaceok(a,n,c)

fori=1,n-1do--длякаждойужеразмещеннойкоролевы

if(a[i]==c)or--тотжестолбец?

(a[i]-i==c-n)or--тажедиагональ?

(a[i]+i==c+n)then--тажедиагональ?

returnfalse--позицияможетбытьатакована

end

end

returntrue--никакихатак;позициявпорядке

end

--печатаетдоску

localfunctionprintsolution(a)

fori=1,Ndo

forj=1,Ndo

io.write(a[i]==jand"X"or"-","")

end

io.write("\n")

end

io.write("\n")

end

--добавляетнадоску'a'всехкоролевот'n'до'N'

localfunctionaddqueen(a,n)

ifn>Nthen--былиразмещенывсекоролевы?

printsolution(a)

else--пытаетсяразместитьn-уюкоролеву

forc=1,Ndo

ifisplaceok(a,n,c)then

a[n]=c--помещаетn-уюкоролевувстолбце'c'

addqueen(a,n+1)

end

end

end

end

--запускаетпрограмму

addqueen({},1)

Далееунасидетфункцияprintsolution,котораяпечатаетдоску.Онапростообходитвсюдоску,печатая 'X' напозицияхскоролевойи '-' надругихпозициях.Каждыйрезультатвыглядитпримернотак:

X-------

----X---

Page 133: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

-------X

-----X--

--X-----

------X-

-X------

---X----

Последняя функция addqueen является ядром программы. Онаприменяет отслеживание в обратном порядке для поиска правильныхрешений. Сначала она проверяет, является ли решение законченным, иесли да, то печатает это решение.В противном случае она перебираетвсе столбцы; для каждого столбца, свободного от атак, программапомещаеттудакоролевуирекурсивнопытаетсяразместитьоставшихсякоролев.

Наконец, главное тело программы просто вызывает addqueen дляначаларешениязадачи.

10.2.Самыечастовстречающиесяслова

Наш следующий пример— это простая программа, которая читаеттекстипечатаетсамыечастовстречающиесявэтомтекстеслова.

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

Прочестьтекст,посчитавчисловхожденийкаждогослова.Отсортироватьсписоксловвнисходящемпорядкепоихчастотности.Напечататьпервыеnэлементовотсортированногосписка.

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

localcounter={}

forwinallwordsdo

counter[w]=(counter[w]or0)+1

end

Обратитевниманиенатрюксorдляобработкинеинициализированныхсчетчиков.

Page 134: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Следующим шагом будет сортировка списка слов. Однако, каквнимательный читатель мог заметить, у нас нет списка слов длясортировки! Несмотря на это, его легко создать, используя слова,которыепредставленыключамивтаблицеcounter:

localwords={}

forwinpairs(counter)do

words[#words+1]=w

end

Кактолькомыполучимэтотсписок,мыможемотсортироватьегоприпомощи предопреленной функции table.sort, которую мы краткообсуждаливглаве6:

table.sort(words,function(w1,w2)

returncounter[w1]>counter[w2]or

counter[w1]==counter[w2]andw1<w2

end)

Слова с наибольшими значениями счетчиков идут первыми; слова сравнымизначениямисчетчиковидутвалфавитномпорядке.

Законченная программа приведена в листинге 10.2. Обратитевнимание на применение сопрограммы для выворачивания наизнанкуитератора auxwords, который использован в следующем цикле. Впоследнем цикле, печатающем результат, программа считает, что еепервый аргумент — это число слов, которое нужно напечатать, и поумолчаниюиспользуетзначение10,еслиаргументовпереданонебыло.

Листинг10.2.Программа«частотностьслов»localfunctionallwords()

localauxwords=function()

forlineinio.lines()do

forwordinstring.gmatch(line,"%w+")do

coroutine.yield(word)

end

end

end

returncoroutine.wrap(auxwords)

end

localcounter={}

forwinallwords()do

counter[w]=(counter[w]or0)+1

end

localwords={}

Page 135: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

forwinpairs(counter)do

words[#words+1]=w

end

table.sort(words,function(w1,w2)

returncounter[w1]>counter[w2]or

counter[w1]==counter[w2]andw1<w2

end)

fori=1,(tonumber(arg[1])or10)do

print(words[i],counter[words[i]])

end

10.3.АлгоритмцепиМаркова

Нашпоследний пример— это реализацияалгоритма цепиМаркова.Программагенерируетпсевдослучайныйтекстнаоснованиитого,какиесловамогутследоватьзапоследовательностьюизnпредыдущихсловвбазовомтексте.Дляэтойреализациимыбудемсчитать,чтоnравно2.

Первая часть читает базовый текст и строит таблицу, которая длякаждогопрефиксаиздвухсловдаетсписоквсехслов,которыевтекстеследуют за этим префиксом. После построения таблицы программаиспользует ее для генерации случайного текста, где каждое словоследуетзадвумяпредыдущимистойжевероятностью,чтоивбазовомтексте. В результате мы получаем случайный на вид текст. Например,применив эту программу к английскому тексту данной книги, мыполучим фрагменты вроде «Constructors can also traverse a tableconstructor,thentheparenthesesinthefollowinglinedoesthewholefileinafieldntostorethecontentsofeachfunction,buttoshowitsonlyargument.Ifyouwanttofindthemaximumelementinanarraycanreturnboththemaximumvalueandcontinues showing thepromptand running thecode.The followingwords are reserved and cannot be used to convert between degrees andradians».

Мы будем кодировать каждый префикс при помощи его двух слов,соединенныхпосредствомпробела:

functionprefix(w1,w2)

returnw1..""..w2

end

МывоспользуемсястрокойNOWORD (перевод строки) для инициализациипрефиксныхсловиобозначенияконцатекста.Например,длятекста"the

Page 136: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

morewetrythemorewedo" таблица последующих слов выглядела бытакимобразом:

{["\n\n"]={"the"},

["\nthe"]={"more"},

["themore"]={"we","we"},

["morewe"]={"try","do"},

["wetry"]={"the"},

["trythe"]={"more"},

["wedo"]={"\n"},

}

Программахранитсвоютаблицувпеременнойstatetab.Длявставкинового слова в список префиксов данной таблицы, мы используемследующуюфункцию:

functioninsert(index,value)

locallist=statetab[index]

iflist==nilthen

statetab[index]={value}

else

list[#list+1]=value

end

end

Сначалаонапроверяет,естьлиужеуданногопрефиксасписок;еслинет,то создает новый список с новым значением. В противном случае онавставляетновоезначениевконецсуществующегосписка.

Для построения таблицы statetab мы будем использовать двепеременныеw1иw2,содержащиедвапоследнихпрочитанныхслова.Длякаждого нового прочитанного слова мы добавляем его к списку,связанномусw1иw2,азатемобновляемw1иw2.

Послепостроениятаблицыпрограмманачинаетгенерироватьтекст,состоящий из MAXGEN слов. Для начала она заново инициализируетпеременные w1 и w2. Затем для каждого префикса она наугад выбираетследующее слово из списка допустимых последующих слов, печатаетэто слово и обновляет w1 и w2. Листинги 10.3 и 10.4 содержатзаконченнуюпрограмму.Вотличиеотнашегопредыдущегопримера снаиболее часто встречающимися словами, здесь мы используемреализациюallwords,основаннуюназамыканиях.

Листинг 10.3. Вспомогательные определения дляпрограммы«цепьМаркова»

functionallwords()

Page 137: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localline=io.read()--текущаястрока

localpos=1--текущаяпозициявстроке

returnfunction()--итерирующаяфункция

whilelinedo--повторяет,покаестьстроки

locals,e=string.find(line,"%w+",pos)

ifsthen--словойнайдено?

pos=e+1--обновляетследующую

позицию

returnstring.sub(line,s,e)--возвращаетслово

else

line=io.read()--словоненайдено;пробуетследующую

строку

pos=1--перезапускспервойпозиции

end

end

returnnil--строкбольшенет;конецобхода

end

end

functionprefix(w1,w2)

returnw1..""..w2

end

localstatetab={}

functioninsert(index,value)

locallist=statetab[index]

iflist==nilthen

statetab[index]={value}

else

list[#list+1]=value

end

end

Листинг10.4.Программа«цепьМаркова»localN=2

localMAXGEN=10000

localNOWORD="\n"

--строиттаблицу

localw1,w2=NOWORD,NOWORD

forwinallwords()do

insert(prefix(w1,w2),w)

w1=w2;w2=w;

end

insert(prefix(w1,w2),NOWORD)

--генирируеттекст

w1=NOWORD;w2=NOWORD--новаяинициализация

fori=1,MAXGENdo

Page 138: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

locallist=statetab[prefix(w1,w2)]

--наугадвыбираетэлементизсписка

localr=math.random(#list)

localnextword=list[r]

ifnextword==NOWORDthenreturnend

io.write(nextword,"")

w1=w2;w2=nextword

end

Упражнения

Упражнение 10.1. Измените программу с восьмью королевами,чтобыонаостанавливаласьпослепечатипервогорешения.

Упражнение 10.2. Альтернативной реализацией задачи о восьмикоролевахбылобыпостроениевсехвозможныхперестановокчиселот1до 8 и проверка на правильность для каждой такой перестановки.Измените программу с применением этого подхода. Как отличаетсябыстродействиеновойпрограммыпосравнениюсостарой?(Подсказка:сравните полное число перестановок с числом раз, когда исходнаяпрограммавызываетфункциюisplaceok.)

Упражнение10.3.Когдамыприменяемпрограммучастотностисловктексту,тообычносамымичастовстречаемымисловамиоказываютсякороткие неинтересные слова вроде артиклей и предлогов. Изменитепрограмму так, чтобы она пропускала слова, состоящие менее чем изчетырехбукв.

Упражнение10.4.Обобщите алгоритмцепиМаркова так, чтобыонмог использовать любой размер для последовательности предыдущихслов,используемойпривыбореследующегослова.

Page 139: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ЧастьII

Таблицыиобъекты

Page 140: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА11

Структурыданных

Таблицы в Lua — это не одна из структур данных, — этоединственная структура данных. Все структуры, которые предлагаютдругиеязыки,—массивы,записи,списки,очереди,множества—могутбыть представлены в Lua при помощи таблиц. Главное, таблицы Luaэффективнореализуютвсеэтиструктуры.

В традиционных языках, таких как С и Pascal, мы реализуембольшинство структур данных при помощи массивов и списков (гдесписки = записи + указатели). Хотя мы можем реализовать массивы исписки при помощи таблиц Lua (и иногда мы это делаем), таблицыгораздо мощнее массивов и списков; многие алгоритмы сиспользованием таблиц становятся до банальности простыми.Например,мыредкопишемалгоритмпоискавLua,посколькутаблицыпредоставляютпрямойдоступклюбомутипу.

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

11.1.Массивы

Мы реализуем массивы в Lua, просто индексируя таблицы целымичислами.Такимобразом,массивынеимеютфиксированногоразмераирастутпомеренеобходимости.Обычноприинициализациимассивамынеявно определяем его размер. Например, после выполненияследующегокодалюбаяпопыткаобратитьсякполювнедиапазона1—1000вернетnilвместо0:

a={}--новыймассив

fori=1,1000do

a[i]=0

end

Page 141: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Операция длины ('#') использует данныйфакт для нахождения размерамассива:

print(#a)-->1000

Вы можете начать массив с нуля, единицы или любого другогозначения:

--создаетмассивсиндексамиот-5до5

a={}

fori=-5,5do

a[i]=0

end

Однако, в Lua принято начинатьмассивы с индекса 1. БиблиотекиLuaпридерживаются этой традиции, как и операция длины. Если вашимассивы не начинаются с 1, то вы не сможете использовать данныесредства.

Мыможемиспользоватьконструктордлясозданияиинициализациимассивоввединомвыражении:

squares={1,4,9,16,25,36,49,64,81}

Подобные конструкторы могут быть настолько большими, насколькоэтонужно(покрайнеймере,донесколькихмиллионовэлементов).

11.2.Матрицыимногомерныемассивы

ЕстьдваосновныхспособапредставленияматрицвLua.Первый—это использовать массив массивов, то есть таблицу, каждый элементкоторой является другой.таблицей. Например, вы можете создатьматрицуизнулейразмеромNнаMприпомощиследующегокода:

mt={}--создаетматрицу

fori=1,Ndo

mt[i]={}--создаетновуюстроку

forj=1,Mdo

mt[i][j]=0

end

end

ПосколькутаблицыявляютсяобъектамивLua,длясозданияматрицывыдолжны явно создавать каждую строку. С одной стороны, этоопределенно более громоздко, чем просто объявить матрицу, как этоделается в языках С и Pascal. С другой стороны, это дает больше

Page 142: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

гибкости.Например,выможетесоздатьтреугольнуюматрицу,изменивциклfor j=1,M do ... end в предыдущемпримеренаfor j=1,i do ...end. С этим кодом треугольная матрица использует лишь половинупамятипосравнениюсисходной.

Второй способ представления матриц в Lua заключается вобъединениидвухиндексовводин.Еслиобаиндексаявляютсяцелымичислами,товыможетепростоумножитьпервыйнасоответствующуюконстанту и затем добавить второй индекс. С этим подходомследующийкодсоздастнашуматрицуизнулейразмеромNнаM:

mt={}--создаетматрицу

fori=1,Ndo

forj=1,Mdo

mt[(i-1)*M+j]=0

end

end

Если индексы являются строками, то вы можете создать одининдекс, соединив эти строки с некоторым символом между ними.Например, вы можете индексировать матрицу m со строковымииндексамиsиtприпомощикодаm[s..":"..t],приусловии,чтоиs,иtне содержат двоеточия; в противном случае пары вроде ("а:","b") и("а",":b") сольются в одининдекс "а::b".Когда сомневаетесь,можетеиспользоватьуправляющийсимволвроде'\0'дляразделенияиндексов.

Довольно часто приложения используют разреженнуюматрицу, тоестьматрицу,гдебольшинствоэлементов—нольилиnil.Например,выможете представить граф при помощи его матрицы смежности, вкоторойзначениенапозицииm,nравнох,еслимеждуузламиmиnестьсоединение ценой х. Когда эти узлы не соединены, то значение напозиции m,n равно nil. Чтобы представить граф с десятью тысячамиузлов,гдекаждыйузелимеетоколопятисоседей,вамнужнаматрицасостамиллионамиэлементов(квадратнаяматрицаиз10000столбцови10000строк),нотолькопримернопятьдесяттысячизнихбудутнеравныnil (пять ненулевых столбцов для каждой строки, соответствующихпяти соседям каждого узла). Многие книги по структурам данныхпространнообсуждают, какможнореализоватьподобныеразреженныематрицы,нетратянаних400Мбпамяти,новамредкопонадобятсяэтиприемы при программировании на Lua. Так как массивы представленытаблицами,онимогутбытьразреженыестественнымобразом.Снашимпервымпредставлением(таблицатаблиц)вампонадобятсядесятьтысячтаблиц, каждая из которых содержит около пяти элементов, то есть

Page 143: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

всегооколопятидесятитысячэлементов.Совторымпредставлениемувас будет одна таблица с пятьюдесятью тысячами элементов.Независимо от представления, вам понадобится память только дляэлементов,отличныхотnil.

При работе с разреженными матрицами мы не можем использоватьоперацию длины из-за дырок (значений nil) между активнымиэлементами.Однако,этонебольшаяпотеря;дажееслибымымоглиегоиспользовать,тоделатьэтогонестоилобы.Длябольшинстваоперацийбыло бы крайне неэффективно перебирать все эти пустые элементы.Вместо этого мы можем использовать pairs для обхода толькоэлементов, отличных от nil. Например, чтобы умножить строку наконстанту,мыможемиспользоватьследующийкод:

functionmult(a,rowindex,k)

localrow=a[rowindex]

fori,vinpairs(row)do

row[i]=v*k

end

end

Однако,примитексведению,чтоуключейнетвнутреннегопорядкавтаблице,поэтомуитерацияприпомощиpairsнегарантирует,чтомыпосетим все столбцы по возрастанию. Для некоторых задач (вроденашего предыдущего примера) это не проблема. Для других задач выможете использовать альтернативный подход, например, связанныесписки.

11.3.Связанныесписки

Поскольку таблицы являются динамическими сущностями, тореализовать связанные списки в Lua довольно легко. Каждый узелпредставлен таблицей, а ссылки являются просто полями таблицы,которые содержат ссылки на другие таблицы. Например, давайтереализуем базовый список, где каждый узел содержит два поля, next иvalue.Корнемспискаявляетсяпростаяпеременная:

list=nil

Длявставкиэлементасозначениемvвначалосписка,мыделаем:list={next=list,value=v}

Дляперебораспискамыпишем:

Page 144: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

locall=list

whileldo

<проверитьl.value>l=l.next

end

Другие разновидности списков, например двунаправленные иликольцевые списки, также легко реализуются. Однако, подобныеструктурывамредкопонадобятсявLua,посколькуобычноестьболеепростой способ представления ваших данных без использованиясвязанных списков. Например, мы можем представить стек в виде(неограниченного)массива.

11.4.Очередиидвойныеочереди

Простейшим способом реализации очередей в Lua являетсяиспользование функций insert и remove из библиотеки table. Этифункции вставляют и удаляют элементы из произвольной позициимассива, сдвигая остальные элементы для согласования действий.Однако, подобные перемещения могут быть дорогими для большихструктур. Более эффективная реализация использует две индекса, одиндляпервогоэлементаиодиндляпоследнего:

functionListNew()

return{first=0,last=-1}

end

Во избежание загрязнения глобального пространства имен мыопределим все операции со списком внутри таблицы, которую мысоответственно назовем List (таким образом, мы создадим модуль).Тогдамыперепишемнашпоследнийпримерследующимобразом:

List={}

functionList.new()

return{first=0,last=-1}

end

Теперь мы можем вставлять и удалять элементы с обоих концов запостоянноевремя:

functionList.pushfirst(list,value)

localfirst=list.first-1

list.first=first

list[first]=value

end

Page 145: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionList.pushlast(list,value)

locallast=list.last+1

list.last=last

list[last]=value

end

functionList.popfirst(list)

localfirst=list.first

iffirst>list.lastthenerror("listisempty")end

localvalue=list[first]

list[first]=nil--чтобыразрешитьсборкумусора

list.first=first+1

returnvalue

end

functionList.poplast(list)

locallast=list.last

iflist.first>lastthenerror("listisempty")end

localvalue=list[last]

list[last]=nil--чтобыразрешитьсборкумусора

list.last=last-1

returnvalue

end

Если вы будете использовать эту структуру для обслуживания впорядкепоступления,вызываятолькоpushlastиpopfirst, тоиfirst, иlastбудутпостояннорасти.Однако,таккакмыпредставляеммассивывLuaприпомощитаблиц,выможетеиндексироватьихкакс1до20,таки с 16 777 216 до 16 777 236. Поскольку Lua использует дляпредставления чисел двойную точность, ваша программа можемвыполняться на протяжении двухсот лет, делая помиллиону вставок всекунду,преждечемвозникнетпроблемаспереполнением.

11.5.Множестваимультимножества

Предположим, вы хотите составить список всех идентификаторов,используемых в программе; каким-то образом вам нужноотфильтровывать зарезервированные слова при составлении вашегосписка. Некоторые программисты на С могут поддаться искушениюпредставитьмножествозарезервированныхсловввидемассивастрокизатем для проверки обыскивать этот массив, чтобы узнать, есть лизаданное слово в этом множестве. Чтобы ускорить поиск, дляпредставления множества они даже могут воспользоваться бинарнымдеревом.

Page 146: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

В Lua эффективным и простым способом представить такиемножествабудетпоместитьихэлементывтаблицувкачествеиндексов.Тогда вместо поиска заданного элемента в таблице вы всего лишьиндексируетеэтутаблицуипроверяете,равенлиполученныйрезультатnil.Внашемпримеремымоглибынаписатьследующийкод:

reserved={

["while"]=true,["end"]=true,

["function"]=true,["local"]=true,

}

forwinallwords()do

ifnotreserved[w]then

<сделатьчто-нибудьс'w'>--'w'—незарезервированноеслово

end

end

(ПосколькуэтисловазарезервированывLua,мынеможемиспользоватьих в качестве идентификаторов; например, мы не можем написатьwhile=true.Вместоэтогомыпишем["while"]=true.)

Вы можете получить более понятную инициализацию при помощидополнительнойфункциидляпостроениямножества:

functionSet(list)

localset={}

for_,linipairs(list)doset[l]=trueend

returnset

end

reserved=Set{"while","end","function","local",}

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

functioninsert(bag,element)

bag[element]=(bag[element]or0)+1

end

Дляудаленияэлементамыуменьшаемегосчетчикнаединицу:functionremove(bag,element)

localcount=bag[element]

Page 147: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

bag[element]=(countandcount>1)andcount-1ornil

end

Мы храним счетчик, только если он уже существует и по-прежнемубольшенуля.

11.6.Строковыебуферы

Допустим,выстроитестрокупочастям,например,построчночитаяфайл.Вашкодвыгляделбыпримерноследующимобразом:

localbuff=""

forlineinio.lines()do

buff=buff..line.."\n"

end

Несмотря на его безобидный вид, этот код может сильно ударить побыстродействиюдлябольшихфайлов:например,чтениефайларазмером1Мбзанимает1,5минутынамоемстаромПентиуме(Примечание:«МойстарыйПентиум»—этокомпьютерсодноядерным32-битовымPentiumчастотой3ГГц.Вседанныеобыстродействиивэтойкнигеполученынанем).

Почему так? Чтобы понять, что происходит, представим, что мынаходимсявсерединециклачтения;каждаястрокаизфайласостоитиз20байтов,имыужепрочли2500этихстрок,поэтомуbuff—этострокаразмером 50 Кб. Когда Lua соединяет buff..line.."\n"; она выделяетновуюстрокуразмером50020байтикопирует50000байтизbuffвэтуновую строку. Таким образом, для каждой новой строки из файла Luaперемещаетвпамятипримерно50Кб,иэтотразмертолькорастет.Болееточно,этоталгоритмимеетквадратичнуюсложность.Послепрочтения100новыхстрок(всего2Кб)изфайлаLuaужепереместилболее2Мбпамяти.КогдаLuaзавершитчтение350Кб,онпереместитболее50Гб.(Эта проблема свойственна не только Lua: другие языки, где строкинеизменяемы, также сталкиваются с подобной проблемой. НаиболееизвестнымпримеромтакогоязыкаявляетсяJava).

Преждечеммыпродолжим,необходимозаметить,что,несмотрянавсе сказанное, данная ситуация не является распространеннойпроблемой. Для маленьких строк вышеприведенный цикл отличноработает. Для чтения всего файла Lua предоставляет опциюio.read("*а"), с которойфайл читается за один раз.Однако, иногда отэтой проблемы никуда не деться. Для борьбы с ней Java использует

Page 148: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

структуруstringBuffer.ВLua в качестве строкового буферамыможемиспользовать таблицу. В основе этого подхода лежит функцияtable.concat, которая возвращает результат конкатенации всех строк иззаданного списка. При помощи concat мы можем переписать нашпредыдущийциклследующимобразом:

localt={}

forlineinio.lines()do

t[#t+1]=line.."\n"

end

locals=table.concat(t)

Этот алгоритм требует менее 0,5 секунды для чтения того же самогофайла, которому требовалась почти минута с первоначальным кодом.(Несмотря на это, для чтения всего файла по-прежнему лучшеиспользоватьio.readсопцией"*а".)

Можно сделать еще лучше. Функция concat принимает второйнеобязательный аргумент, который является разделителем для вставкимеждустроками.Сэтимразделителемнамненужновставлятьпереводстрокипослекаждойизних:

localt={}

forlineinio.lines()do

t[#t+1]=line

end

s=table.concat(t,"\n").."\n"

Функцияconcatвставляетразделительмеждустроками,нонамвсеравнонужно добавить один последний перевод строки. Эта последняяконкатенация копирует итоговую строку, что может быть довольнодолго. Не существует опции, чтобы добиться от concat вставкидополнительного разделителя, номыможем обмануть ее, добавив в tлишнююпустуюстроку:

t[#t+1]=""

s=table.concat(t,"\n")

Дополнительныйпереводстроки,которыйconcatдобавитпередпустойстрокой,будетнаходитьсявконцеитоговойстроки,какмыихотели.

11.7.Графы

Как и любой практичный язык, Lua предлагает ряд реализаций дляграфов, каждый из которых лучше приспособлен под какие-то

Page 149: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

специфические алгоритмы. Здесь мы рассмотрим простую объектно-ориентированную реализацию, в которой мы будем представлять узлыкакобъекты(нуразумеется,таблицы),аребракакссылкимеждуузлами.

Мы будем представлять каждый узел как таблицу с двумя полями:name—имяузла,иadj—множество соседнихснимузлов.Посколькумы будем читать граф из текстового файла, нам будет нужен способнайти узел по его имени. Для этого мы будем использоватьдополнительную таблицу, отображающую имена на узлы. Функцияname2nodeвозвращаетузел,получивегоимя:

localfunctionname2node(graph,name)

localnode=graph[name]

ifnotnodethen

--узелнесуществует;создадимновыйузел

node={name=name,adj={}}

graph[name]=node

end

returnnode

end

Листинг 11.1 содержит функцию, которая строит граф. Она читаетфайл, где каждая строка содержит имена двух узлов, тем самымобозначая ребро от первого узла ко второму. Для каждой строки онаиспользует string.match, чтобы разбить строку на два имени, а затемнаходит соответствующие этим именам узлы (создавая эти узлы принеобходимости)исоединяетих.

Листинг11.1.Чтениеграфаизфайлаfunctionreadgraph()

localgraph={}

forlineinio.lines()do

--делитстрокунадваимени

localnamefrom,nameto=string.match(line,"(%S+)%s+(%S+)")

--находитсоответственныеузлы

localfrom=name2node(graph,namefrom)

localto=name2node(graph,nameto)

--добавляет'to'ксмежномумножеству'from'

from.adj[to]=true

end

returngraph

end

Листинг 11.2 иллюстрирует алгоритм с применением подобныхграфов.Функцияfindpath ищет путь между двумя узлами при помощиобхода графов в глубину. Ее первый параметр — это текущий узел;

Page 150: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

второй задает целевой узел; третий параметр хранит путь от начала ктекущему узлу; последний параметр — это множество всех ужепосещенных узлов (чтобы избежать циклов). Обратите внимание, какалгоритмнапрямуюработает с узлами, избегаяиспользованияихимен.Например,visited—этомножествоузлов,анеименузлов.Аналогичноpath—этосписокузлов.

Листинг11.2.Нахождениепутимеждудвумяузламиfunctionfindpath(curr,to,path,visited)

path=pathor{}

visited=visitedor{}

ifvisited[curr]then--узелужепосетили?

returnnil--путиздесьнет

end

visited[curr]=true--помечаетузелкакпосещенный

path[#path+1]=curr--добавляетегокпути

ifcurr==tothen--конечныйузел?

returnpath

end

--пробуетвсесоседниеузлы

fornodeinpairs(curr.adj)do

localp=findpath(node,to,path,visited)

ifpthenreturnpend

end

path[#path]=nil--удаляетузелизпути

end

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

functionprintpath(path)

fori=1,#pathdo

print(path[i].name)

end

end

g=readgraph()

a=name2node(g,"a")

b=name2node(g,"b")

p=findpath(a,b)

ifpthenprintpath(p)end

Упражнения

Упражнение 11.1. Измените реализацию очереди так, чтобы обаиндексавозвращалисьнаноль,когдаочередьпуста.

Page 151: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнение 11.2. Повторите упражнение 10.3, но вместоиспользования длины в качестве критерия для отбрасывания словданная программа должна прочесть из текстового файла списокотбрасываемыхслов.

Упражнение11.3.Изменитеструктуруграфатак,чтобыонахраниламеткудлякаждогоребра.Даннаяструктуратакжедолжнапредставлятькаждоереброприпомощиобъектасдвумяполями:егометкойиузлом,на который он указывает. Вместо множества соседних узлов каждыйузел должен хранить множество соседних ребер, берущих начало изэтогоузла.

Адаптируйте функцию readgraph так, чтобы она из каждой строкивходногофайлачиталадваимениузловиметку. (Допустим,чтометкаявляетсячислом).

Упражнение 11.4. Предположим, представление графа изпредыдущего упражнения устроено так, что метка каждого ребрапредставляетсобойрасстояниемеждуегоконечнымиузлами.Напишитефункцию, которая находит кратчайший путь между двумя заданнымиузлами.(Подсказка:используйтеалгоритмДейкстры.)

Page 152: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА12

Файлысданнымиисохраняемость

Приобработкефайловсданнымиобычногораздопрощеписатьэтиданные, чем их читать. Когда мы пишем в файл, мы полностьюконтролируемвсе,чтопроисходит.Сдругойстороны,когдамычитаемиз файла, то мы не знаем, чего ждать. Помимо обработки всех видовданных, которые могут содержаться в правильном файле, устойчиваяпрограмматакжедолжнадостойносправлятьсясдефектнымифайлами.Поэтому написание устойчивых к ошибкам программ для чтенияданныхвсегдасложно.

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

12.1.Файлысданными

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

Как обычно, чтобы стало понятнее, давайте рассмотрим пример.Если у нашего файла с данными предопределенный формат, такой какCSV (Comma-Separated Values — значения, разделенные запятыми) илиXML, то наш выбор крайне мал. Однако, если мы хотим создать файлдля нашего собственного использования, то мы в качестве нашегоформата можем использовать конструкторы Lua. В этом формате мыпредставляемкаждыйэлементданныхввидеконструктораLua.Вместозаписивнашфайлчего-товроде

DonaldE.Knuth,LiterateProgramming,CSLI,1992

JonBentley,MoreProgrammingPearls,Addison-Wesley,1990

мыпишем:

Page 153: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Entry{"DonaldE.Knuth",

"LiterateProgramming",

"CSLI",

1992}

Entry{"JonBentley",

"MoreProgrammingPearls",

"Addison-Wesley",

1990}

Вспомним,чтоEntry{код}—этотожесамое,чтоиEntry({код)),тоесть вызов функции Entry с таблицей в качестве единственногоаргумента. Поэтому вышеприведенный фрагмент данных являетсяпрограммой Lua. Для чтения этого файла нам лишь нужно выполнитьего, имея подходящее определение для Entry. Например, следующаяпрограммасчитаетчислозаписейвфайлесданными:

localcount=0

functionEntry()count=count+1end

dofile("data")

print("numberofentries:"..count)

Следующая программа создает множество из всех имен авторов,найденныхвфайле,ипечатаетих(необязательновтомжепорядке,чтоивфайле):

localauthors={}--множестводляхраненияименавторов

functionEntry(b)authors[b[1]]=trueend

dofile("data")

fornameinpairs(authors)doprint(name)end

Обратите внимание на событийно-ориентированный подход в этихфрагментах кода: Entry выступает в роли функции обратного вызова,котораявызываетсявовремяработыdofileдлякаждойзаписивфайлесданными.

Когда нас не волнует размер файла, мы можем в качестве нашегопредставленияиспользоватьпарыимя-значение(Примечание:ЕслиэтотформатнапоминаетвамBibTeX,тоэтонеслучайность.ФорматBibTeXпослужилоднимизпрототиповдлясинтаксисаконструктороввLua):

Entry{

author="DonaldE.Knuth",

title="LiterateProgramming",

publisher="CSLI",

year=1992

}

Page 154: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Entry{

author="JonBentley",

title="MoreProgrammingPearls",

year=1990,

publisher="Addison-Wesley",

}

Этот формат мы называем форматом самоописываемых данных,посколькукаждыйфрагментданныхсодержиткраткоеописаниесвоегозначения. Самоописываемые данные более читаемы (по крайней мере,людьми),чемCSVилидругиекомпактныеформаты;принеобходимостиих легко отредактировать вручную; и они позволяют нам вноситьнебольшие изменения в базовый формат, не требуя изменять файлы сданными.Например, еслимыдобавимновоеполе, то нампотребуетсявнестилишьнебольшоеизменениевчитающуюпрограмму,чтобыонапредоставлялазначениепоумолчанию,когдаполенеуказано.

Припомощиформатаимя-значениенашапрограммадлясоставленияспискаавторовстановитсяследующей:

localauthors={}--множестводляхраненияименавторов

functionEntry(b)authors[b.author]=trueend

dofile("data")

fornameinpairs(authors)doprint(name)end

Теперь порядок полей не важен. Даже если у некоторых записей нетавтора,тонампонадобитсяизменитьлишьфункциюEntry:

functionEntry(b)

ifb.authorthenauthors[b.author]=trueend

end

Lua не только быстро выполняется, но и быстро компилируется.Например,вышеприведеннаяпрограммадлясоставленияспискаавторовобрабатывает1Мбданныхзаоднудесятуюсекунды. (Примечание:Намоем старом Пентиуме.) И это не случайно. Описание данных былоодной из главных областей применения Lua с момента его создания, имы уделяем огромное внимание тому, чтобы его компилятор работалбыстродлябольшихпрограмм.

12.2.Сериализация

Частонамнужносериализоватькакие-тоданные,тоестьперевестиэтиданныевпотокбайтовилисимволов,чтобымымоглисохранитьихвфайлилипослатьпо сети.Мыможемпредставлять сериализованные

Page 155: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

данные в виде кода Lua так, чтобы при выполнении этого кода онвоссаздавалсохраненныезначениядлясчитывающейпрограммы.

Обычно если мы хотим восстановить значение глобальнойпеременной,тонашкусоккодабудетчем-товродеvarname=выражение,гдевыражение—этокоднаLuaдляполучениязначения.Сvarnameвсепросто,поэтомудавайтепосмотрим,какнаписатькод,которыйсоздаетзначение.Длячисловогозначениязадачапроста:

functionserialize(o)

iftype(o)=="number"then

io.write(o)

else<прочиеслучаи>end

end

Темнеменее,при записичиславдесятичномвиде естьрискпотерятьточность. В Lua 5.2 можно использовать шестнадцатеричный формат,чтобыизбежатьподобнойпроблемы:

iftype(o)=="number"then

io.write(string.format("%a",o))

Сэтимформатом("%а")прочитанноечислобудетсостоятьровноизтехжебитов,чтоиисходное.

Длястроковогозначениянаивныйподходвыгляделбыпримернотак:iftype(o)=="string"then

io.write("'",o,"'")

Однако,еслистрокасодержитспециальныесимволы(такиекаккавычкиили переводы строк), то итоговый код уже не будет допустимойпрограммойLua.

Может показаться заманчивым решить эту проблему путемизменениявидакавычек:

iftype(o)=="string"then

io.write("[[",o,"]]")

Но так делать не стоит. Если какой-то пользователь со злым умысломсумеет заставить вашу программу сохранить что-то вроде "]]..os.execute('rm *')..[[ " (например, передав данную строку вкачествесвоегоадреса),товрезультатевашимкускомкодабудет

varname=[[]]..os.execute('rm*')..[[]]

Page 156: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Припопыткезагрузитьэти«данные»васждетнеприятныйсюрприз.Простымибезопаснымспособомзаключитьстрокувкавычкибудет

использование опции "%q" из функции string.format. Она окружаетстрокудвойнымикавычкамиикорректноэкранируетдвойныекавычки,переводыстрокинекоторыедругиесимволывнутриэтойстроки:

a='a"problematic"\\string'

print(string.format("%q",a))-->"a\"problematic\"\\string"

С данной возможностью наша функция serialize теперь выглядитследующимобразом:

functionserialize(o)

iftype(o)=="number"then

io.write(o)

elseiftype(o)=="string"then

io.write(string.format("%q",o))

else<прочиеслучаи>end

end

Начинаясверсии5.1Luaпредлагаетдругойвариантдлябезопасногозаключения произвольных строк в кавычки — при помощи записи [=[...]=] для длинных строк. Однако, эта новая запись в основномпредназначена для собственноручно написанного кода, когда мы влюбомслучаенехотимизменятьстроковыелитерала.Вавтоматическигенерируемом коде легче экранировать проблематичные символы, какэтоделаетопция"%q"изstring.format.

Еслижевывсеравнохотитеиспользоватьдлинностроковуюзаписьдля автоматически генерируемого кода, то вам нужно позаботиться онекоторых деталях. Первая деталь состоит в том, что вам нужноподобратьправильноечислознаковравенства.Подходящеечисло—то,которое больше максимальной длины их последовательности висходной строке. Поскольку строки, содержащие длинные цепочки иззнаков равенства, не являются редкостью (например, комментарии,разделяющие фрагменты исходного кода), мы можем ограничитьсярассмотрением последовательностей знаков равенства, заключенныхмежду квадратными скобками; другие последовательности не могутпривести к ошибочному маркеру конца строки. Второй детальюявляется то, что Lua всегда отбрасывает перевод строки в началедлиннойстроки;простымметодомборьбысэтимявляетсядобавлениепереводастроки,которыйбудетотброшен.

Page 157: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг 12.1. Заключение в кавычки произвольныхстроковыхлитералов

functionquote(s)

--находитмаксимальнуюдлинупоследовательностейиззнаков

равенства

localn=-1

forwinstring.gmatch(s,"]=*]")do

n=math.max(n,#w-2)---2дляудалениявсех']'

end

--производитстрокус'n'плюсодинзнаковравенства

localeq=string.rep("=",n+1)

--строитстрокувкавычках

returnstring.format("[%s[\n%s]%s]",eq,s,eq)

end

Функция quote из листинга 12.1 является результатом нашихпредыдущих замечаний. Она принимает произвольную строку ивозвращает ее отформатированной как длинную строку. Вызовstring.gmatch создаетитератордляперебора всехпоследовательностейобразца']=*]'(тоестьзакрывающейквадратнойскобки,послекоторойстоит последовательность из ноля или больше знаков равенства, закоторымиследуетещеодназакрывающаяквадратнаяскобка)встрокеs.(Примечание: Мы обсудим сопоставление с образцом в главе 21.) Длякаждого вхождения цикл обновляет n до максимального на данныймомент числа знаков равенства. После этого цикла мы используемфункциюstring.rep,чтобыповторитьзнакравенстваn+1раз,тоестьнаодин знак больше, чем у максимальной последовательности,встреченной в этой строке. Наконец, string.format заключает s междупарами квадратных скобок с надлежащим числом знаков равенствавнутри и добавляет дополнительные пробелы вокруг помещаемой вкавычкистрокиипереводстрокивначалесодержащейеестроки.

Сохранениетаблицбезциклов

Нашей следующей (и более сложной) задачей является сохранениетаблиц.Существуетнесколькоспособовсохраненияихвсоответствиистем, какие ограничения мы накладываем на структуру таблицы. Несуществуетодного алгоритма, которыйбыподходилдля всех случаев.Дело не только в том, что простым таблицам нужны более простые

Page 158: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

алгоритмы,ноивтом,чтополучающиесяприэтомфайлымогутбытьболеенаглядны.

Нашаследующаяпопыткапредставленавлистинге12.2.Несмотрянасвою простоту, эта функция довольно практична. Она дажеобрабатывает вложенные таблицы (то есть таблицы внутри другихтаблиц)дотехпор,покаструктуратаблицыявляетсядеревом(тоестьне содержит общих подтаблиц и циклов). Небольшим визуальнымулучшением будет добавление отступов к иногда встречающимсявложеннымтаблицам(см.упражнение12.1).

Листинг12.2.Сериализациятаблицбезцикловfunctionserialize(o)

iftype(o)=="number"then

io.write(o)

elseiftype(o)=="string"then

io.write(string.format("%q",o))

elseiftype(o)=="table"then

io.write("{\n")

fork,vinpairs(o)do

io.write("",k,"=")

serialize(v)

io.write(",\n")

end

io.write("}\n")

else

error("cannotserializea"..type(o))

end

end

Предыдущая функция предполагает, что все ключи в таблицеявляются допустимыми идентификаторами. Если в таблице естьчисловые ключи или строковые ключи, которые не являютсясинтаксически допустимыми идентификаторами в Lua, то у наспроблема. Простым путем ее разрешения является использованиеследующегокодадлязаписикаждогоключа:

io.write("[");serialize(k);io.write("]=")

С этим изменением мы увеличили надежность нашей функции за счетнаглядностиполучающегосяфайла.Рассмотримследующийвызов:

serialize{a=12,b='Lua',key='another"one"'}

Результат этого вызова при использовании первой версии serialize

Page 159: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

будетследующим:{

a=12,

b="Lua",

key="another\"one\"",

}

Сравнитесрезультатомприиспользованиивторойверсии:{

["a"]=12,

["b"]="Lua",

["key"]="another\"one\"",

}

Мыможемполучитьинадежность,инаглядность,проверяявкаждомконкретном случае необходимость квадратных скобок; и вновь мыоставимэтоулучшениевкачествеупражнения.

Сохранениетаблицсциклами

Для обработки таблиц с общей топологией (то есть с циклами иобщимиподтаблицами)нампотребуетсядругойподход.Конструкторыне могут представлять подобные таблицы, поэтому мы их и не будемиспользовать. Для представления циклов нам нужны имена, поэтомунаша следующая функция в качестве аргументов получит значение,котороеследуетсохранить,иимя.Болеетого,мыдолжныотслеживатьимена уже сохраненных таблиц, чтобы переиспользовать их приобнаружении цикла. Для этого отслеживания мы воспользуемсядополнительной таблицей. Эта таблица будет содержать таблицы вкачествеиндексовиихименавкачествесвязанныхснимизначений.

Листинг12.3.СохранениетаблицсцикламиfunctionbasicSerialize(o)

iftype(o)=="number"then

returntostring(o)

else--предположим,чтоэтострока

returnstring.format("%q",o)

end

end

functionsave(name,value,saved)

saved=savedor{}--начальноезначение

io.write(name,"=")

Page 160: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

iftype(value)=="number"ortype(value)=="string"then

io.write(basicSerialize(value),"\n")

elseiftype(value)=="table"then

ifsaved[value]then--значениеужесохранено?

io.write(saved[value],"\n")--используетегопрежнее

имя

else

saved[value]=name--сохраняетимядля

другогораза

io.write("{}\n")--создаетновуютаблицу

fork,vinpairs(value)do--сохраняетееполя

k=basicSerialize(k)

localfname=string.format("%s[%s]",name,k)

save(fname,v,saved)

end

end

else

error("cannotsavea"..type(value))

end

end

Итоговый код показан в листинге 12.3 . Мы придерживаемсяограничения,чтотаблицы,которыемыхотимсохранить,содержатлишьчисла и строки в качестве ключей.Функция basicSerialize сериализуетэти базовые типы, возвращая результат. Следующая функция, save,выполняетвсютяжелуюработу.Параметрsaved—этотаблица,котораяотслеживает уже сохраненные таблицы. Например, если мы построимтаблицутак

a={x=1,y=2;{3,4,5}}

a[2]=a--цикл

a.z=a[1]--общаяподтаблица

товызовsave("а",а)сохранитееследующимобразом:a={}

a[1]={}

a[1][1]=3

a[1][2]=4

a[1][3]=5

a[2]=a

a["y"]=2

a["x"]=1

a["z"]=a[1]

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

Page 161: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

предыдущий узел, необходимый для нового определения, былопределензаранее.

Еслимыхотимсохранитьнесколькозначенийсобщимичастями,томыможемсделатьнескольковызововsaveприпомощитойжетаблицыsaved.Например,предположим,унасестьследующиедветаблицы:

a={{"one","two"},3}

b={k=a[1]}

Еслимысохранимихнезависимодруготдруга,тоурезультатанебудетобщихчастей:

save("a",a)

save("b",b)

-->a={}

-->a[1]={}

-->a[1][1]="one"

-->a[1][2]="two"

-->a[2]=3

-->b={}

-->b["k"]={}

-->b["k"][1]="one"

-->b["k"][2]="two"

Однако, если мы используем ту же самую таблицу saved для обоихвызововsave,тоурезультатабудутобщиечасти:

localt={}

save("a",a,t)

save("b",b,t)

-->a={}

-->a[1]={}

-->a[1][1]="one"

-->a[1][2]="two"

-->a[2]=3

-->b={}

-->b["k"]=a[1]

КакпринятовLua,естьнесколькодругихвариантов.Онипозволяютсохранятьзначениебезвыдачиемуглобальногоимени(например,кусокстроит локальное значение и возвращает его), обрабатывать функции(путем построения вспомогательной таблицы, связывающей каждуюфункциюсееименем)ит.д.Luaдаетвамсилу;механизмыстроитевы.

Упражнения

Page 162: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнение12.1.Изменитекодизлистинга12.2,чтобыондобавлялотступыковложеннымтаблицам.

(Подсказка: добавьте дополнительный параметр к serialize в видестрокисотступом.)

Упражнение 12.2. Измените код из листинга 12.2, чтобы ониспользовалсинтаксис["key"]=valueтак,какпредложеновразделе12.1.

Упражнение 12.3. Измените код предыдущего упражнения так,чтобы он использовал синтаксис ["key"]=value, только принеобходимости (то есть когда ключ в виде строки не являетсядопустимымидентификатором).

Упражнение12.4.Изменитекодпредыдущегоупражнения,чтобыониспользовал синтаксис конструкторов для списков везде, где этовозможно. Например, он должен сериализовать таблицу {14,15,19} как{14,15,19}, а не как {[1]=14,[2]=15,[3]=19}. (Подсказка: начните ссохранения значений ключей 1, 2,..., если только они не равны nil.Позаботьтесь о том, чтобы не сохранить их снова при перебореостальнойчаститаблицы.)

Упражнение 12.5. Подход с отказом от конструкторов присохранениитаблицсцикламислишкомрадикальный.Можносохранитьтаблицу в более приятном виде, применяя конструкторы в общемслучае,азатемиспользуяприсваиваниятолькодляисправленияобщегодоступаициклов.

Зановореализуйтефункциюsaveсиспользованиемданногоподхода.Добавьте к ней все вкусности, что вы уже реализовали в предыдущихупражнениях(оступы,синтаксисзаписейисинтаксиссписков).

Page 163: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА13

Метатаблицыиметаметоды

Обычно для каждого значения в Lua есть вполне предсказуемыйнабор операций. Мы можем складывать числа, соединять строки,вставлять пары ключ-значение в таблицыи т. д.Однако,мынеможемскладывать таблицы, сравнивать функции и вызывать строку. Еслитолькомынеиспользуемметатаблицы.

Метатаблицы позволяют изменить поведение значения при еговстрече с неожиданной операцией. Например, при помощиметатаблицмыможемопределитьто,какLuaвычисляетвыражениеа+b,гдеаиb—это таблицы. Когда Lua пытается сложить две таблицы, он проверяет,есть ли хотя бы в одной из них метатаблица и содержит ли этаметатаблица поле __add. Если Lua находит это поле, он вызываетсоответствующее значение — так называемый метаметод, которыйдолженбытьфункцией,—длявычислениясуммы.

КаждоезначениевLuaможетиметьсвязаннуюснимметатаблицу.Утаблиц и пользовательских данных отдельные метатаблицы; значенияостальных типов совместно используют одну единственнуюметатаблицу для всех значений своего типа. Lua всегда создает новыетаблицыбезметатаблиц:

t={}

print(getmetatable(t))-->nil

Мы можем использовать функцию setmetatable, чтобы задать илиизменитьметатаблицудлялюбойтаблицы:

t1={}

setmetatable(t,t1)

print(getmetatable(t)==t1)-->true

ИзLuaмыможемустанавливатьметатаблицытолькодлятаблиц;дляработы с метатаблицами значений других типов мы должныиспользовать код С (Примечание: Главная причина этого ограничениясостоит в том, чтобы помешать злоупотреблению метатаблиц дляраспространенных типов. Опыт предыдущих версий Lua показал, чтоподобныеглобальныеизменениячастоведуткодноразовомукоду).Как

Page 164: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

мы позже увидим в главе 21, строковая библиотека устанавливаетметатаблицу для строк. У всех прочих типов по умолчанию нетметатаблиц:

print(getmetatable("hi"))-->table:0x80772e0

print(getmetatable("xuxu"))-->table:0x80772e0

print(getmetatable(10))-->nil

print(getmetatable(print))-->nil

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

13.1.Арифметическиеметаметоды

Вэтомразделемыприведемпростойпример,чтобыобъяснить,какиспользовать метатаблицы. Пусть мы используем таблицы дляпредставлениямножествсфункциямидлявычисленияихобъединения,пересеченияит.п.,какпоказановлистинге13.1.Чтобынезасорятьнашепространствоимен,мыбудемхранитьэтифункциивтаблицеSet.

Листинг13.1.ПростаяреализациямножествSet={}

--создаетновоемножествосозначениямииззаданногосписка

functionSet.new(l)

localset={}

for_,vinipairs(l)doset[v]=trueend

returnset

end

functionSet.union(a,b)

localres=Set.new{}

forkinpairs(a)dores[k]=trueend

forkinpairs(b)dores[k]=trueend

returnres

end

functionSet.intersection(a,b)

localres=Set.new{}

forkinpairs(a)do

res[k]=b[k]

Page 165: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

end

returnres

end

--представляетмножествокакстроку

functionSet.tostring(set)

locall={}--listtoputallelementsfromtheset

foreinpairs(set)do

l[#l+1]=e

end

return"{"..table.concat(l,",").."}"

end

--печатаетмножество

functionSet.print(s)

print(Set.tostring(s))

end

Теперь нам нужно использовать операцию сложения ('+') длявычисления объединения двух множеств. Для этого мы дадимвозможносьвсемтаблицам,представляющиммножества,использоватьодну общую метатаблицу. Эта метатаблица определит, как таблицыдолжны реагировать на операцию сложения. Нашим первым шагомбудетсозданиеобычнойтаблицы,которуюмыбудемиспользоватькакметатаблицудлямножеств:

localmt={}--метатаблицадлямножеств

Следующим шагом будет изменение функции Set.new, создающеймножества. В новой версии этой функции будет лишь однадополнительнаястрока,котораядлясоздаваемыхтаблицустанавливаетmtкакметатаблицу:

functionSet.new(l)--втораяверсия

localset={}

setmetatable(set,mt)

for_,vinipairs(l)doset[v]=trueend

returnset

end

После этого каждое множество, которое мы создадим при помощиSet.new,будетиметьоднуитужетаблицувкачествеметатаблицы:

s1=Set.new{10,20,30,50}

s2=Set.new{30,1}

print(getmetatable(s1))-->table:00672B60

print(getmetatable(s2))-->table:00672B60

Page 166: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Наконец, мы добавим к метатаблице метаметод в виде поля __add,котороеопределяет,какнужновыполнятьсложение:

mt.__add=Set.union

Послеэтогокаждыйраз,когдаLuaпопытаетсясложитьдвамножества,он будет вызывать функцию Set.union, передавая оба операнда вкачествеаргументов.

С метаметодом мы можем использовать операцию сложения длявыполненияобъединениямножеств:

s3=s1+s2

Set.print(s3)-->{1,10,20,30,50}

Аналогично мы можем определить операцию умножения длявыполненияпересечениямножеств:

mt.__mul=Set.intersection

Set.print((s1+s2)*s1)-->{10,20,30,50}

Длякаждойарифметическойоперациисуществуетсоответствующееимяполявметатаблице.Кроме__addи__mulесть__sub(длявычитания),__div(дляделения),__unm (дляотрицания),__mod (длявзятияостаткаотделения) и __pow (для возведения в степень). Мы также можемопределить поле __concat для описания поведения операцииконкатенации.

Когда мы складываем два множества, то вопрос о том, какуюметатаблицу использовать, не возникает. Однако, мы можем записатьвыражение, в котором участвуют два значения с разнымиметатаблицами,напримертакое,какниже:

s=Set.new{1,2,3}

s=s+8

ПрипоискеметаметодаLuaвыполняетследующиешаги:еслиупервогозначенияестьметатаблицасполем__add, тоLuaиспользуетэтополевкачестве метаметода независимо от второго значения; иначе, если увторого значения есть метатаблица с полем __add, Lua использует егополе в качестве метаметода; в противном случае Lua вызовет ошибку.Такимобразом,впоследнемпримеребудетвызванаSet.union,какидлявыражений10+sи"hello"+s.

Lua не беспокоится об этих смешанных типах, но они важны длянашей реализации. Например, если мы выполним s=s+8, то получим

Page 167: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ошибкувнутриSet.union:badargument#1to'pairs'(tableexpected,gotnumber)

Еслинамнужныболеепонятныесообщенияобошибках,томыдолжныявнопроверятьтипыоперандовпередпопыткойвыполненияоперации:

functionSet.union(a,b)

ifgetmetatable(a)~=mtorgetmetatable(b)~=mtthen

error("attemptto'add'asetwithanon-setvalue",2)

end

<какпрежде>

Помните, что второй аргумент функции error (2 в нашем случае)направляет сообщение об ошибке туда, где данная операция былавызвана.

13.2.Метаметодысравнения

Метатаблицытакжепозволяютпридатьсмыслоперациямсравненияпосредством метаметодов __eq (равно), __lt (меньше, чем) и __lе

(меньше или равно, чем). Для трех оставшихся операций сравнения нетотдельныхметаметодов:Luaпереводита~=bвnot(а==b),а>bвb<аиа>=bвb<=а.

До версии 4.0 Lua переводил все операции упорядочивания в одну,переводяа<=bвnot(b<а).Однако, такойпереводнекорректен,когдамыимеем дело с частичным порядком, то есть когда не все элементынашего типа надлежащим образом упорядочены. Например, числа сплавающей точкой не являются полностью упорядоченными набольшинстве компьютеров из-за значения NaN (Not a Number — нечисло). В соответствии со стандартом IEEE 754 NaN представляетнеопределенные значения, такие как результат 0/0. Согласно стандарту,любое сравнение, включающее в себяNaN, должнобыть ложным.Этозначит,чтоNaN<=xвсегдаложно,ноиx<=NaNтакжеложно.Изэтоготакжеследует,чтоперевода<=bвnot(b<a)вданномслучаеневерен.

В нашем примере с множествами мы имеем дело с похожейпроблемой. Очевидным (и полезным) значением <= для множествявляется вхождение множества: а<=b означает, что а — этоподмножествоb.С этимзначениемвсеравновозможно,чтоа<=b иb<аложны; таким образом, нам нужны отдельные реализации для __lе(меньшеилиравно)и__lt(меньше,чем):

Page 168: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

mt.__le=function(a,b)--вхождениемножеств

forkinpairs(a)do

ifnotb[k]thenreturnfalseend

end

returntrue

end

mt.__lt=function(a,b)

returna<=bandnot(b<=a)

end

Наконец, мы можем определить равенство множеств через ихвхождение:

mt.__eq=function(a,b)

returna<=bandb<=a

end

Послеэтихопределениймыготовысравниватьмножества:s1=Set.new{2,4}

s2=Set.new{4,10,2}

print(s1<=s2)-->true

print(s1<s2)-->true

print(s1>=s1)-->true

print(s1>s1)-->false

print(s1==s2*s1)-->true

Длятиповсполнымпорядкоммыможемнеопределятьметаметод__lе.ПриегоотсутствииLuaиспользуетполе__lt.

Усравнениянаравенствоестьнекоторыеограничения.Еслиудвухобъектовразныебазовыетипыилиметаметоды,тооперациясравнениянаравенствовернетfalse,даженевызываяметаметоды.Такимобразоммножество всегда будет отличаться от числа, независимоот того, чтоговоритметаметод.

13.3.Библиотечныеметаметоды

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

Функция tostring является типичным примером. Как мы видели

Page 169: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ранее,tostringпредставляеттаблицыдовольнопростымобразом:print({})-->table:0x8062ac0

Функция print всегда вызывает tostring для форматирования своеговывода. Однако, при форматировании какого-либо значения tostringсначалапроверяет,естьлиузначенияметаметод__tostring.Еслитакойметаметод есть, то tostring вызывает его, передавая ему объект вкачествеаргумента.То,чтовернетэтотметаметод,ибудетрезультатомtostring.

Внашемпримере смножествамимыуже определилифункциюдляпредставления множества в виде строки. Поэтому нам нужно лишьвыставитьполе__tostringвэтойметатаблице:

mt.__tostring=Set.tostring

После этого, когда бы мы не вызвали print с множеством в качествеаргумента, print вызовет tostring, которая, в свою очередь, вызоветSet.tostring:

s1=Set.new{10,4,5}

print(s1)-->{4,5,10}

Функции setmetatable и getmetatable также используют метаполе, вданном случае для защиты метатаблиц. Предположим, вы хотитезащититьвашимножестватак,чтопользователинесмогутниувидеть,ни изменить их метатаблицы. Если задать в метатаблице поле__metatable,тоgetmetatable вернет значение этогополя, в то времякакsetmetatableвызоветошибку:

mt.__metatable="notyourbusiness"

s1=Set.new{}

print(getmetatable(s1))-->notyourbusiness

setmetatable(s1,{})

stdin:1:cannotchangeprotectedmetatable

В Lua 5.2 pairs и ipairs также обладают метатаблицами, поэтомутаблица может изменить способ своего обхода (или можно добавитьобходдляобъектов,неявляющихсятаблицами).

13.4.Метаметодыдоступактаблице

Метаметоды для арифметических операций и операций сравнения

Page 170: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

лишьопределяютповедениедляситуаций,которыеиначеприводилибыквозникновениюошибок.Ониневлияютнаобычноеповедениеязыка.НоLuaтакжепредлагаетспособдляизмененияповедениятаблицвдвухобычных случаях: запросе и изменении несуществующего поля втаблице.

Метаметод__index

Ранее я уже говорил, что когда мы обращаемся к отсутствующемуполювтаблице,результатомявляетсяnil.Этотак,ноэтоневсяправда.Насамомделеприподобномобращенииинтерпретаторначинаетискатьметаметод __index: если такого метода нет, что обычно и бывает, товозвращается nil; иначе результат будет предоставлен этимметаметодом.

Типичнымпримером здесь является наследование.Пустьмы хотимсоздатьнесколькотаблиц,описывающихокна.Каждаятаблицадолжнаописывать различные параметры окна, такие как положение, размер,цветовая схема и т. п. Для всех этих параметров есть значения поумолчаниюипоэтомумыхотимстроитьобъектыокон,задаваятолькоте значения, которые отличаются от значений по умолчанию. Первыйвариант — предоставить конструктор, заполняющий отсутствующиеполя. Второй вариант — сделать так, чтобы новые окна наследовалилюбоеотсутствующееполеотпрототипаокон.Дляначаламыобъявимпрототип и функцию-конструктор, которая создает новые окна,обладающиеобщейметатаблицей:

--создаетпрототипсозначениямипоумолчанию

prototype={x=0,y=0,width=100,height=100}

mt={}--создаетметатаблицу

--объявляетфункцию-конструктор

functionnew(o)

setmetatable(o,mt)

returno

end

Теперьмыопределимметаметод__index:mt.__index=function(_,key)

returnprototype[key]

end

После этого кода мы создадим новое окно и обратимся котсутствующемуполю:

Page 171: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

w=new{x=10,y=20}

print(w.width)-->100

Lua обнаружит, что у w нет требуемого поля, но есть метатаблица сполем __index. Поэтому Lua вызовет этот метаметод с аргументами w(таблица) и "width" (отсутствующий ключ). Затем метаметодиндексируетэтотпрототипзаданнымключомивозвращаетполученноезначение.

Использованиеметаметода__indexдлянаследованиявLuaнастолькораспространено,чтоLuaпредоставляетсокращенныйвариант.Хотяегоизовутметодом,метаметод__indexнеобязанбытьфункцией:например,онможетбытьтаблицей.Когдаонявляетсяфункцией,Luaвызываетего,передавая таблицу и отсутствующий ключ в качестве аргументов, какмытолькочтовидели.Когдаонявляетсятаблицей,Luaперенаправляетк ней обращение. Поэтому в нашем предыдущем примере мы моглипростоопределить__indexследующимобразом:

mt.__index=prototype

Теперь, когда Lua будет искать метаметод __index, он найдет значениеprototype, которое является таблицей. Соответственно, Lua повторитобращениекэтойтаблице,тоестьвыполнитаналогprototype["width"].Этообращениеиприведеткнужномурезультату.

Использованиетаблицывкачествеметаметода__indexдаетпростойибыстрыйспособреализацииодиночногонаследования.Функция,хотяи более затратна, предоставляет больше гибкости: мы можемреализовать множественное наследование, кэширование и ряд другихвариантов.Мыобсудимэтиформынаследованиявглаве16.

Когда мы хотим обратиться к таблице без вызова ее метаметода__index,мыиспользуемфункциюrawget.Вызовrawget(t,i)осуществляетнепосредственныйдоступктаблицеt,тоестьпримитивноеобращениебезиспользованияметатаблиц.Применениенепосредственногодоступане ускорит ваш код (затраты на вызов функции уничтожат любуюприбавку),ноиногда,какмыувидимпозже,оннеобходим.

Метаметод__newindex

Метаметод__newindex делает тоже, что и __index, но работает приобновлениях таблиц, а не при доступе к ним. Когда вы присваиваетезначение отсутствующему индексу в таблице, интерпретатор ищет

Page 172: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

метаметод __newindex: если он есть, то интерпретатор вызывает еговместо выполнения присваивания. Подобно __index, если метаметодявляетсятаблицей,тоинтерпретаторвыполняетприсваиваниедляэтойтаблицы вместо исходной. Более того, есть функция с прямымдоступом, которая позволяет миновать метаметод: rawset (t, k, v)

записывает значение v по ключу k в таблицу t, не вызывая никакихметаметодов.

Совместное использование метаметодов __index и __newindex

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

Таблицысозначениямипоумолчанию

Значениепоумолчаниюдлялюбогополявобычнойтаблице—этоnil.Этолегкоизменитьприпомощиметатаблиц:

functionsetDefault(t,d)

localmt={__index=function()returndend}

setmetatable(t,mt)

end

tab={x=10,y=20}

print(tab.x,tab.z)-->10nil

setDefault(tab,0)

print(tab.x,tab.z)-->100

После вызова setDefault любой доступ к отсутствующему полю в tabвызовет его метаметод __index, который вернет ноль (значение d дляэтогометаметода).

Функция setDeafult создает новое замыкание и новую метатаблицудля каждой таблицы, которой нужно значение по умолчанию., Этоможет оказаться затратным, если у нас много таблиц, которым нужнызначения по умолчанию. У метатаблицы значение по умолчанию d«зашито»вееметаметод,поэтомумынеможемиспользоватьоднуитужеметатаблицудлявсехтаблиц.Чтобыможнобылоиспользоватьоднуи тужеметатаблицу для таблиц с разными значениямипо умолчанию,мы можем запоминать значение по умолчанию в самой таблице,

Page 173: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

используя для этого специальное поле. Если мы не беспокоимся оконфликтах имен, мы можем использовать для нашего особого поляключвроде"___":

localmt={__index=function(t)returnt.___end}

functionsetDefault(t,d)

t.___=d

setmetatable(t,mt)

end

Обратите внимание, что теперьмысоздаем таблицуmt лишьодин раз,внефункцииsetDefault.

Еслимыбеспокоимсяоконфликтахимен,томожнолегкоубедитьсяв уникальности особого ключа. Все, что нам нужно, — это создатьновуютаблицуииспользоватьеевкачествеключа:

localkey={}--уникальныйключ

localmt={__index=function(t)returnt[key]end}

functionsetDefault(t,d)

t[key]=d

setmetatable(t,mt)

end

Другим способом связывания значения по умолчанию с каждойтаблицей является использование отдельной таблицы, где индексыявляются этими таблицами, а значения этих индексов являютсязначениями этих таблиц по умолчанию. Однако, для корректнойреализацииданногоподходанамнужнаособаяразновидностьтаблиц—так называемые слабые таблицы (weak tables), поэтому здесь мы небудемегоиспользовать;мывернемсякданнойтемевглаве17.

Иной подход состоит в том, чтобы запоминать (memorize)метатаблицы, чтобы затем многократно использовать одну и ту жеметатаблицу для таблиц с одинаковыми значениями по умолчанию.Однако, для этого тоже нужны слабые таблицы, поэтому нам вновьпридетсяподождатьдоглавы17.

Отслеживаниедоступактаблице

И __index, и __newindex работают только при отсутствии в таблицесоответствующегоиндекса.Поэтомуединственныйспособотслеживатьвесь доступ к таблице— это держать ее пустой. Таким образом, еслимы хотим отслеживать весь доступ к таблице, нам нужно создатьпосредника(proxy) для настоящей таблицы.Данныйпосредник— это

Page 174: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

пустаятаблицассоответствующимиметаметодами__indexи__newindexдля отслеживания доступа к таблице, которые будут перенаправлятьдоступкисходнойтаблице.Пустьt—этоисходнаятаблица,доступккотороймыхотимотслеживать.Тогдамыможемнаписатьчто-товродеэтого:

t={}--исходнаятаблица(созданнаягде-тоеще)

--хранитзакрытыйдоступкпервоначальнойтаблице

local_t=t

--создаетпосредника

t={}

--создаетметатаблицу

localmt={

__index=function(t,k)

print("*accesstoelement"..tostring(k))

return_t[k]--доступкисходнойтаблице

end,

__newindex=function(t,k,v)

print("*updateofelement"..tostring(k)..

"to"..tostring(v))

_t[k]=v--обновляетисходнуютаблицу

end

}

setmetatable(t,mt)

Этоткодотслеживаеткаждоеобращениекt:>t[2]="hello"

*updateofelement2tohello

>print(t[2])

*accesstoelement2

hello

Если нам требуется обойти эту таблицу, мы должны определить впосредникезапись__pairs:

mt.__pairs=function()

returnfunction(_,k)

returnnext(_t,k)

end

end

Намможетпонадобитьсянечтоподобноедля__ipairs.Еслимыхотимследитьзанесколькимитаблицами,тонамненужно

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

Page 175: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

иразделитьоднуобщуюметатаблицумеждувсемипосредниками.Этопохоже на задачу связывания таблицы с ее значениями по умолчанию,которую мы обсуждали в предыдущем разделе. Например, можнохранить исходную таблицу в поле посредника при помощиспециальногоключа.Результатомявляетсяследующийкод:

localindex={}--создаетзакрытыйиндекс

localmt={--создаетметатаблицу

__index=function(t,k)

print("*accesstoelement"..tostring(k))

returnt[index][k]--доступкисходнойтаблице

end,

__newindex=function(t,k,v)

print("*updateofelement"..tostring(k)..

"to"..tostring(v))

t[index][k]=v--обновляетисходнуютаблицу

end,

__pairs=function(t)

returnfunction(t,k)

returnnext(t[index],k)

end,t

end

}

functiontrack(t)

localproxy={}

proxy[index]=t

setmetatable(proxy,mt)

returnproxy

end

Теперь, всякий раз, когда нам потребуется отслеживать таблицу t, все,чтонамнужнобудетсделать,—выполнитьt=track(t).

Таблицы,доступныетолькодлячтения

Можно легко адаптировать идею посредников, чтобы реализоватьтаблицы,доступныетолькодлячтения(read-only).Все,чтонамнужно,—этовызыватьошибкукаждыйраз,когдамыловимпопыткуобновитьтаблицу.Дляметаметода__indexмыможемиспользоватьсамуисходнуютаблицу вместо функции, так как нам не нужно отслеживать запросы;проще и эффективнее перенаправлять такие запросы сразу к исходнойтаблице. Однако, это потребует новой метатаблицы для каждогодоступного только для чтения посредника, с полем __index,указывающимнаисходнуютаблицу:

Page 176: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionreadOnly(t)

localproxy={}

localmt={--создаетметатаблицу

__index=t,

__newindex=function(t,k,v)

error("attempttoupdatearead-onlytable",2)

end

}

setmetatable(proxy,mt)

returnproxy

end

Вкачествепримераиспользования таких таблицмыможем создатьтаблицуназванийднейнедели:

days=readOnly{"Sunday","Monday","Tuesday","Wednesday",

"Thursday","Friday","Saturday"}

print(days[1])-->Sunday

days[2]="Noday"

stdin:1:attempttoupdatearead-onlytable

Упражнения

Упражнение13.1.Определитеметаметод__sub,которыйвозвращаетразницу двух множеств. (Множество а— b является множеством всехэлементовиза,которыенесодержатсявb.)

Упражнение 13.2. Определите для множеств метаметод __len так,чтобы#sвозвращалчислоэлементоввмножествеs.

Упражнение13.3.Дополнитереализациюпосредниковвразделе13.4метаметодом__ipairs.

Упражнение 13.4. Другим способом реализации таблиц, доступныхтолько для чтения, является использование функции в качествеметаметода __index. Этот подход делает обращения к таблице болеезатратными, но создание таких таблиц обходится дешевле, так как всетаблицы,доступныетолькодлячтения,могутсовместноиспользоватьодну метатаблицу. Перепишите функцию readOnly с использованиемданногоподхода.

Page 177: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА14

Окружение

Lua хранит все свои глобальные переменные в обычной таблице,называемой глобальным окружением (global environment). (Точнее, Luaхранитсвои«глобальные»переменныевнесколькихокружениях,номынекоторое время не будем обращать внимание на данные сложности.)Однимизпреимуществ этойструктурыявляется то, чтоонаупрощаетвнутреннюю реализацию Lua, поскольку нет необходимости вспециальной структуре данных для глобальных переменных. Другоепреимуществосостоитвтом,чтомыможемработатьсэтойтаблицейтакже,какислюбойдругой.ДляупрощениятакойработыLuaхранитсамо окружение в глобальной переменной _G. (Да, _G._G равно _G.)Например,следующийкодпечатаетименавсехглобальныхпеременных,определенныхвглобальномокружении:

forninpairs(_G)doprint(n)end

Вэтойглавемыувидимнесколькополезныхметодовдляработысокружением.

14.1.Глобальныепеременныесдинамическимиименами

Обычно для обращения к глобальным переменным и установки ихзначений достаточно присваивания. Однако, довольно часто намтребуетсякакая-либоформаметапрограммирования,например,когдамыхотимработатьсглобальнойпеременной,чьеимясодержитсявдругойпеременной или каким-то образом вычисляется во время выполнения.Чтобы получить значение такой переменной, многие программистыпытаютсяписатьнечтоподобное:

value=loadstring("return"..varname)()

Если,скажем,varnameравноx,торезультатомконкатенациибудет"returnх",чтопривыполнениидастнамжелаемыйрезультат.Однако,этоткодвключаетвсебясозданиеикомпиляциюновогокускакода,чтоявляетсядовольнозатратным.Выможетедобитьсятогожеэффектаприпомощи

Page 178: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

следующегокода,которыйвдесяткиразэффективнеепредыдущего:value=_G[varname]

Поскольку окружение — это обычная таблица, то вы можете простоиндексироватьеенужнымключом(именемпеременной).

Похожим образом можно присвоить значение глобальнойпеременной, чье имя вычисляется динамически, если написать_G[varname] = value. Однако, будьте внимательны: некоторыепрограммисты так радуются подобной возможности, что заканчиваютнаписанием кода вроде _G["a"] = _G["var1"], что является всего лишьусложненнымвариантомнаписанияa=var1.

Обобщениемпредыдущейзадачиявляетсяразрешениеиспользованияполей в динамических именах, таких как "io.read" или "а.b.с.d". Еслинаписать _G["io.read"], то очевидно, что мы не получим поле read изтаблицы io. Но мы можем написать функцию getfield, такую чтоgetfield ("io.read") вернет ожидаемое значение. Эта функцияпреимущественноявляетсяциклом,которыйначинаетс_Gиразвиваетсяполезаполем:

functiongetfield(f)

localv=_G--начинаетстаблицыглобальныхпеременных

forwinstring.gmatch(f,"[%w_]+")do

v=v[w]

end

returnv

end

Мыполагаемсянаgmatchизбиблиотекиstringдляпереборавсехсловвf(где«слово»—этопоследовательностьизодногоилиболеебуквенно-цифровыхсимволовизнаковподчеркивания).

Соответствующая функция для задания полей немного сложнее.Присваиваниевродеa.b.c.d=vэквивалентноследующемукоду:

localtemp=a.b.c

temp.d=v

To есть мы должны извлечь последнее имя и затем отдельно егообработать.Следующаяфункцияsetfield решает данную задачу и приэтомсоздаетпромежуточныетаблицывпути,когдаонинесуществуют:

functionsetfield(f,v)

localt=_G--начинаетстаблицыглобальных

переменных

forw,dinstring.gmatch(f,"([%w_]+)(%.?)")do

Page 179: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ifd=="."then--имянепоследнее?

t[w]=t[w]or{}--создаеттаблицуприееотсутствии

t=t[w]--получаеттаблицу

else--последнееимя

t[w]=v--производитприсваивание

end

end

end

Этотновыйобразецзахватываетимяполявпеременнуюwиследующуюза ним необязательную точку в переменную d.(Примечание: Мыподробнообсудимсопоставлениесобразцомвглаве21).Еслизаименемнеследуетточка,тоэтопоследнееимя.

Приналичиивышеприведенныхфункцийследующийвызовсоздаетглобальную таблицу t и таблицу t.х , после чего присваивает t.х.узначение10:

setfield("t.x.y",10)

print(t.x.y)-->10

print(getfield("t.x.y"))-->10

14.2.Объявленияглобальныхпеременных

В Lua глобальным переменным не нужны объявления. Хотя это иудобно для небольших программ, в больших программах простаяопечаткаможетпривестиктруднообнаруживаемымошибкам.Однако,прижеланиимыможемизменитьэтоповедение.ПосколькуLuaхранитсвои глобальные переменные в обычной таблице, мы можемиспользоватьметатаблицыдляизмененияегоповеденияприобращениикглобальнымпеременным.

Первыйподходсостоитвпростомотслеживаниилюбыхобращенийкотсутствующимключамвглобальнойтаблице:

setmetatable(_G,{

__newindex=function(_,n)

error("attempttowritetoundeclaredvariable"..n,2)

end,

__index=function(_,n)

error("attempttoreadundeclaredvariable"..n,2)

end,

})

После выполнения этого кода любая попытка обратиться кнесуществующей глобальной переменной приведет к возникновению

Page 180: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ошибки:>print(a)

stdin:1:attempttoreadundeclaredvariablea

Но как же нам объявлять глобальные переменные? Одним извариантовявляетсяиспользованиеrawset,котораяминуетметаметод:

functiondeclare(name,initval)

rawset(_G,name,initvalorfalse)

end

(Конструкция or false следит за тем, чтобы глобальная переменнаявсегдаполучалазначение,отличноеотnil.)

Вариант попроще — разрешить присваивания новым глобальнымпеременным только внутри функций, при этом не ограничиваяприсваиваниязапределамикуска.

Для проверки того, что присваивание происходит в главном куске,нам нужно использовать отладочную библиотеку. Вызовdebug.getinfo(2,"S")возвращаеттаблицу,чьеполеwhatсообщитотом,является ли функция, вызвавшая метаметод, главным куском, обычнойфункцией Lua или функцией C. (Мы обсудим debug.getinfo болееподробновглаве24.)Посредствомэтойфункциимыможемпереписатьметаметод__newindexследующимобразом:

__newindex=function(t,n,v)

localw=debug.getinfo(2,"S").what

ifw~="main"andw~="C"then

error("attempttowritetoundeclaredvariable"..n,2)

end

rawset(t,n,v)

end

Эта новая версия также допускает присваивания из кода C, так какобычноегоавторызнают,чтоониделают.

Для проверки существования переменной мы не можем простосравнить ее с nil, поскольку если она nil, то обращение приведет кошибке. Вместо этого мы используем rawget, которая избегаетметаметод:

ifrawget(_G,var)==nilthen

--'var'необъявлена

...

end

Пока что наша схема не допускает использование глобальных

Page 181: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

переменных со значением nil, поскольку они автоматически будутсчитаться необъявленными.Но это не сложно исправить. Все, что намнужно, — это вспомогательная таблица, которая хранит именаобъявленных переменных. При вызове метаметода он по этой таблицепроверяет — объявлена эта переменная, или нет. Ее код может бытьпохожнаприведенныйвлистинге14.1.Теперьдажеприсваиваниявродеx=nilдостаточно,чтобыобъявитьглобальнуюпеременную.

Листинг14.1.ПроверкаописанийглобальныхпеременныхlocaldeclaredNames={}

setmetatable(_G,{

__newindex=function(t,n,v)

ifnotdeclaredNames[n]then

localw=debug.getinfo(2,"S").what

ifw~="main"andw~="C"then

error("attempttowritetoundeclaredvariable"..n,2)

end

declaredNames[n]=true

end

rawset(t,n,v)--производитнастоящуюустановкузначений

end,

__index=function(_,n)

ifnotdeclaredNames[n]then

error("attempttoreadundeclaredvariable"..n,2)

else

returnnil

end

end,

})

Затраты на оба решения крайне малы. При первом решении внормальном режиме работы метаметод вообще не вызывается. Привтором решении метаметоды могут быть вызваны, но только когдапрограммаобращаетсякпеременнойсозначениемnil.

В стандартную поставку Lua входит модуль strict.lua, которыйреализует проверку глобальных переменных, по сути состоящую изтолькочторассмотренногонамикода.ИспользоватьегоприразработкекоданаLua—хорошаяпривычка.

14.3.Неглобальныеокружения

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

Page 182: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Любоеегоизменениевлияетнавсечастивашейпрограммы.Например,когда вы устанавливаете метатаблицу для управления глобальнымдоступом, вся ваша программа должна ей руководствоваться. Если выхотите воспользоваться библиотекой, которая использует глобальныепеременныебезихобъявления,товамнеповезло.

В Lua глобальные переменные не обязаны быть действительноглобальными. Мы даже можем сказать, что в Lua нет глобальныхпеременных. Поначалу это может звучать странно, поскольку мыпользовались глобальнымипеременнымивсе это время.Очевидно,Luaочень старается создать иллюзию наличия глобальных переменных.Давайте посмотрим, как Lua создает эту иллюзию. (Примечание:Обратите внимание, что этот механизм был одной из тех частей Lua,которыепретерпелинаибольшиеизмененияприпереходесверсии5.1на5.2. Следующее обсуждение относится только к Lua 5.2 и очень малоприменимокпредыдущимверсиям.)

Начнемспонятиясвободныхимен.Свободноеимя(freename)—этоимя,котороенепривязанокявномуобъявлению,тоестьневстречаетсявнутри области видимости локальной переменной (или переменнойциклаfor,илипараметра)сэтимименем.Например,иvar1,иvar2—этосвободныеименавследующемкуске:

var1=var2+3

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

_ENV.var1=_ENV.var2+3

Ночтотакоеэтановаяпеременная_ENV?Онанеможетбытьглобальнойпеременной, иначе мы снова возвращаемся к исходной проблеме. Ивновьэтопроделкикомпилятора.Яужеотметил,чтоLuaрассматриваеткаждыйкусоккаканонимнуюфункцию.НасамомделеLuaкомпилируетнашисходныйкусоквследующийкод:

local_ENV=<какое-нибудьзначение>returnfunction(...)

_ENV.var1=_ENV.var2+3

end

To есть Lua компилирует любой кусок кода с участием

Page 183: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

предопределенноговерхнегозначениясименем_ENV.Обычно когда мы загружаем кусок кода, функция load

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

local_ENV=<глобальноеокружение>returnfunction(...)

_ENV.var1=_ENV.var2+3

end

Результатом всех этих присваиваний является то, что поле var1 изглобальногоокруженияполучаетзначениеполяvar2плюс3.

На первый взгляд это может показаться довольно запутаннымспособом работы с глобальными переменными. Я не буду утверждать,что это простейший способ, но он дает гибкость, которую труднополучитьсболеепростойреализацией.

Преждечеммыпродолжим,давайтекраткосформулируемобработкуглобальныхпеременныхвLua5.2:

Luaкомпилируетлюбойкусоквнутриобластивидимостиверхнегозначениясименем_ENV.Компиляторпереводитлюбоесвободноеимяvarв_ENV.var.Функцияload(илиloadfile)инициализируетпервоеверхнеезначениекускаприпомощиглобальногоокружения.

Вконцеконцов,всенетакужисложно.Некоторых это смущает, поскольку они пытаются выявить особые

механизмы, лежащие в основе этих правил. Но никаких особыхмеханизмовнет.Вчастности,запервыедваправилаполностьюотвечаеткомпилятор. За исключением того, что _ENV предопределенакомпилятором, она является обычной переменной. Вне компиляции у_ENV нет никакого особого назначения в Lua. (Примечание: Если бытьчестными до конца, то Lua использует это имя для сообщений обошибках, чтобы в докладе об ошибке с участием переменной _ENV.xуказыватьеекакglobalх.)Точнотакжепереводизvarв_ENV.var—этопростаясинтаксическаязаменабезскрытогосмысла.Вчастности,послеэтогоперевода_ENVбудетотноситьсялюбойпеременной_ENV, котораявиднанаэтомэтапекода,исходяизстандартныхправилвидимости.

Page 184: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

14.4.Использование_ENV

В этом разделе мы увидим некоторые способы для освоения тойгибкости, которую дает нам _ENV. Имейте в виду, что большинствопримеров из данного раздела должно выполняться отдельным куском.При построчном вводе кода в интерактивном режиме каждая строкастановится отдельным куском и таким образом получает своюпеременную_ENV.Длявыполненияфрагментакодакакотдельногокускавамнужнолибозапуститьегоизфайла,либовинтерактивномрежимепоместитьвнутрьпарыdo—end.

Поскольку_ENV—этообычнаяпеременная,мыможемобращатьсякней и присваивать значения как и любой другой переменной.Присваивание _ENV=nil сведет на нет любой прямой доступ кглобальным переменным в оставшейся части куска. Это можетпригодиться для контроля над тем, какие переменные использует вашкод:

localprint,sin=print,math.sin

_ENV=nil

print(13)-->13

print(sin(13))-->0.42016703682664

print(math.cos(13))--ошибка!

Любоеприсваиваниесвободномуименивызоветаналогичнуюошибку.Мыможемнаписать_ENVявнымобразом,чтобыминоватьлокальное

объявление:a=13--глобальная

locala=12

print(a)-->12(локальная)

print(_ENV.a)-->13(глобальная)

Конечно, главной областью применения _ENV является изменениеокружения, используемого фрагментом кода. Как только вы изменитеваше окружение, все обращения к глобальным переменным будутпользоватьсяновойтаблицей:

--изменяеттекущееокрущениенановуюпустуютаблицу

_ENV={}

a=1--создаетполев_ENV

print(a)

-->stdin:4:attempttocallglobal'print'(anilvalue)

Если новое окружение пусто, то вы теряете доступ ко всем вашим

Page 185: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

a=15--создаетглобальнуюпеременную

_ENV={g=_G}--изменяеттекущееокружение

a=1--создаетполев_ENV

g.print(a)-->1

g.print(g.a)-->15

Теперь, когда вы обращаетесь к «глобальной» g, вы получаете староеокружение,вкоторомвынайдетефункциюprint.

Мыможемпереписатьпредыдущийпример,используяимя_Gвместоg:

a=15--создаетглобальнуюпеременную

_ENV={_G=_G}--изменяеттекущееокружение

a=1--создаетполев_ENV

_G.print(a)-->1

_G.print(_G.a)-->15

Для Lua _G — такое же имя, как и все остальные. Его особый статуспроявляется только тогда, когда Lua создает исходную глобальнуютаблицуиприсваиваетэтутаблицуглобальнойпеременной_G.ДляLuaне важно текущее значение этой переменной. Но обычно принятоиспользовать одно и тоже имя всякий раз, когда у нас есть ссылка наглобальноеокружение,каквпереписанномпримере.

Другой способ заполнить ваше новое окружение — применитьнаследование:

a=1

localnewgt={}--создаетновоеокружение

setmetatable(newgt,{__index=_G})

_ENV=newgt--устанавливаетего

print(a)-->1

Вэтомкоденовоеокружениенаследуетизстарогоиprint,иа.Несмотряна это, любое присваивание поступает в новую таблицу. Теперьизменение переменной в глобальном окружении по ошибке не опасно,хотявыпо-прежнемуможетеизменятьихчерез_G:

--продолжаемпредыдущийкод

a=10

print(a)-->10

print(_G.a)-->1

_G.a=20

print(_G.a)-->20

Page 186: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Поскольку _ENV является обычной переменной, она подчиняетсяобычным правилам видимости. В частности, функции, определенныевнутрикуска,обращаютсяк_ENVтакже,какиклюбойдругойвнешнейпеременной:

_ENV={_G=_G}

localfunctionfoo()

_G.print(a)--скомпилированокак

'_ENV._G.print(_ENV.a)'

end

a=10--_ENV.a

foo()-->10

_ENV={_G=_G,a=20}

foo()-->20

Еслимыопределимновуюлокальнуюпеременнуюсименем_ENV,тоссылкинасвободныеименабудутпривязаныкней:

a=2

do

local_ENV={print=print,a=14}

print(a)-->14

end

print(a)-->2(возвращениекизначальной_ENV)

Поэтомунесложнопостроитьфункциюсзакрытымокружением:functionfactory(_ENV)

returnfunction()

returna--"глобальная"a

end

end

f1=factory{a=6}

f2=factory{a=7}

print(f1())-->6

print(f2())-->7

Функция factory создает простые замыкания, которые возвращаютзначение из их глобальных а. При созданном замыкании видимаяпеременная _ENV является параметром _ENV из охватывающей егофункции factory; поэтому замыкание использует эту внешнююпеременную (в качестве верхнего значения) для доступа к своимсвободнымименам.

Используя обычные правила видимости, мы можем работать сокружениями различными способами. Например, у нас может бытьнесколькофункцийсобщимдлянихокружениемилифункция,которая

Page 187: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

изменяетокружение,общеесдругимифункциями.

14.5._ENVиload

Какяранееупоминал,loadобычноинициализируетверхнеезначение_ENV из загруженного куска посредством глобального окружения.Однако,уloadестьнеобязательныйчетвертыйпараметр,которыйзадаетзначениедля_ENV.(Уфункцииloadfileестьаналогичныйпараметр.)

В качестве первого примера допустим, что у нас есть типичныйконфигурационный файл, определяющий различные константы ифункции,используемыепрограммой;этоможетбытьчто-товроде:

--файл'config.lua'

width=200

height=300

...

Мыможемзагрузитьегоприпомощиследующегокода:env={}

f=loadfile("config.lua","t",env)

f()

Весь код из конфигурационного файла будет выполнен в пустомокруженииenv.Важнеето,чтовсеегоопределенияперейдутименновэтоокружение.Этотконфигурационныйфайлникоимобразомнеможетповлиятьначто-либоеще,дажепоошибке.Дажевредоносныйкоднесможетпричинитьмноговреда.Все,чтоонможет,—выполнитьDoS-атаку (приводящую к отказу от обслуживания), тратя процессорноевремяипамять.

Иногда вам может понадобиться выполнить кусок несколько раз,каждый раз с другой таблицей окружения. В этом случаедополнительныйаргументдляloadбесполезен.Вместоэтогоунасестьдвадругихварианта.

Первый вариант — это использовать функцию debug.setupvalue изотладочнойбиблиотеки.Какследуетизимени,setupvalueпозволяетнамизменить любое верхнее значение заданной функции. Следующийфрагментиллюстрируетегоиспользование:

f=loadfile(filename)

...

env={}

debug.setupvalue(f,1,env)

Page 188: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Первый аргумент при вызове setupvalue — это функция, второй —индекс верхнего значения, а третий — новое значение для верхнегозначения. При типичном применении второй аргумент всегда равенединице: когда функция является результатом load или loadfile, Luaследит за тем, чтобы у нее было лишь одно верхнее значение, равное_ENV.

Небольшим минусом данного решения является зависимость ототладочной библиотеки. Эта библиотека нарушает некоторыестандартные соглашения о программах. Например, debug.setupvalue

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

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

_ENV=...;

Вспомним из раздела 8.1, что Lua компилирует любой кусок в видевариадическойфункции.Поэтомуэтадополнительнаястрокаприсвоитпеременной _ENV первый аргумент куска, устанавливая его какокружение. После загрузки этого куска мы вызываем полученнуюфункцию, передавая нужное нам окружение как первый аргумент.Следующий фрагмент кода иллюстрирует эту идею при помощифункцииloadwithprefixизупражнения8.1:

f=loadwithprefix("local_ENV=...;",io.lines(filename,"*L"))

...

env={}

f(env)

Упражнения

Упражнение 14.1. Функция getfield, которую мы определили вначалеэтойглавы,слишкомнеприхотлива,таккаконадопускает«поля»вроде math?sin или string!!!gsub. Перепишите ее так, чтобы онапринимала в качестве разделителяимен только одиночнуюточку. (Дляэтогоупражнениявамможетпонадобитьсяинформацияизглавы21.)

Упражнение14.2.Объяснитевдеталях,чтопроисходитвследующейпрограммеикакимбудетеевывод.

Page 189: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localfoo

do

local_ENV=_ENV

functionfoo()print(X)end

end

X=13

_ENV=nil

foo()

X=0

Упражнение14.3.Объяснитевдеталях,чтопроисходитвследующейпрограммеикакимбудетеевывод.

localprint=print

functionfoo(_ENV,a)

print(a+b)

end

foo({b=14},12)

foo({b=10},1)

Page 190: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА15

Модулиипакеты

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

Начинаясверсии5.1,Luaопределилнаборсоглашенийдлямодулейипакетов(пакетявляетсянабороммодулей).Этисоглашениянетребуютот языка никаких дополнительных средств; программисты могутреализоватьихприпомощитого,чтомыужевидели:таблиц,функций,метатаблиц и окружений. Программисты могут использовать любыедругие соглашения. Разумеется, альтернативные реализации могутпривестиктому,чтопрограммынесмогутиспользоватьчужиемодули,амодулинесмогутбытьиспользованычужимипрограммами.

С точки зрения пользователя,модуль— это некоторый код (на LuaилиС), которыйможет быть загруженпосредством require и которыйсоздает и возвращает таблицу. Все, что модуль экспортирует, будь тофункции или таблицы, он определяет внутри этой таблицы, котораявыступаетвкачествепространстваимен.

Например, все стандартные библиотеки— это модули. Вы можетеиспользоватьматематическуюбиблиотекуследующимобразом:

localm=require"math"

print(m.sin(3.14))

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

math=require"math"

string=require"string"

...

Эта предварительная загрузка позволяет нам записывать math.sinпривычнымобразом.

Page 191: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

Например,дляпользователясуществуетнесколькоспособоввызватьфункциюизмодуля.Обычнымспособомявляетсяследующий:

localmod=require"mod"

mod.foo()

Пользовательможетзадатьдлямодулялюбоелокальноеимя:localm=require"mod"

m.foo()

Также можно предоставить альтернативные имена для отдельныхфункций:

localm=require"mod"

localf=mod.foo

f()

Этисредстваудобнытем,чтоонинетребуютспециальнойподдержкиотязыка.Онииспользуюттолькото,чтоужепредлагаетязык.

Распространеннаяжалобанаrequireсостоитвтом,чтоэтафункцияне может передавать аргументы загружаемому модулю. Например, уматематического модуля могла бы быть опция для выбора междуградусамиирадианами:

--badcode

localmath=require("math","degree")

Проблема в том, что одна из основных задач require — избегатьмногократной загрузкимодуля. Как толькомодуль загружен, он можетбыть многократно использован любой частью программы, которой онснова потребуется. Если бы один и тот же модуль был затребован сразнымипараметрами,этопривелобыкконфликту:

--плохойкод

localmath=require("math","degree")

Page 192: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

--где-тоещевтойжепрограмме

localmath=require("math","radians")

В случае, когда вы действительно хотите, чтобы у вашего модулябыли параметры, лучше создать явную функцию для их задания,напримертак:

localmod=require"mod"

mod.init(0,0)

Еслиинициализирующаяфункциявозвращаетсаммодуль,томыможемнаписатькодвродеследующего:

localmod=require"mod".init(0,0)

Другойвариант—сделатьтак,чтобымодульвозвращалсвоюфункциюдляинициализации,илишьэтафункциявозвращалабытаблицумодуля:

localmod=require"mod"(0,0)

В любом случае помните, что сам модуль загружается всего один раз;разрешениеконфликтныхинициализацийостаетсянаегоусмотрение.

15.1.Функцияrequire

Функцияrequireпытаетсясвестикминимумусвоипредположенияотом,чтоявляетсямодулем.Дляrequireмодуль—этовсеголишькакой-токод,которыйопределяетнекоторыезначения(например,функцииилитаблицы, содержащиефункции).Обычноэтоткодвозвращает таблицу,состоящую из функций этого модуля. Однако, поскольку это делаетсякодом самого модуля, а не require, некоторые модули могут выбратьвозвращатьдругиезначенияилидажеиметьпобочныеэффекты.

Для загрузки модуля мы просто вызываем require "имя_модуля".Первымшагомrequire являетсяпроверкапо таблицеpackage.loaded, небыллизагруженданныймодульранее.Еслида,requireвозвращаетегосоответствующее значение. Поэтому, как только модуль загружен,другиевызовы,длякоторыхонтребуется,простовернуттожезначениебезповтороноговыполнениякакого-либокода.

Если модуль еще не загружен, то require ищет файл Lua с именеммодуля. Если он находит этот файл, то загружает его при помощиloadfile. Результатом этого является функция, которую мы называемзагрузчиком (loader). (Загрузчик — это функция, которая при вызове

Page 193: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

загружаетмодуль.)ЕслиrequireнеможетнайтифайлLuaсименеммодуля,тоонаищет

библиотекуСсэтимименеммодуля.ЕслионанаходитбиблиотекуС,тооназагружаетеепосредствомpackage.loadlib (которуюмыобсудиливразделе8.3)иищетфункциюсименемluaopen_имя_модуля(Примечание:В разделе 27.3 мы обсудим, как писать библиотеки C). В этом случаезагрузчик является результатом loadlib, то есть функциейluaopen_имя_модуля,представленнойвкачествефункцииLua.

Независимо от того, где был найден модуль,— в файле Lua или вбиблиотеке С, у require теперь есть для него загрузчик. Дляокончательной загрузки модуля require вызывает загрузчик с двумяаргументами:именеммодуляиименемфайла,изкоторогобылвзятэтотзагрузчик. (Большинство модулей просто игнорируют эти аргументы.)Еслизагрузчиквозвращаеткакое-либозначение,requireвозвращаетэтозначение и хранит в таблице package.loaded, чтобы всегда возвращатьодинаковые значения при будущих вызовах этого же модуля. Еслизагрузчик не возвращает никакие значения, require ведет себя так, какеслибымодульвернулtrue.Безэтогоуточнения,последующиевызовыrequireсновабывыполнялиэтотмодуль.

Чтобызаставитьrequireзагрузитьодинитотжемодульдважды,мыпростостираемзаписьонемизpackage.loaded:

package.loaded.<имя_модуля>=nil

В следующий раз, когда понадобится этот модуль, require проделаетвсюнеобходимуюработуещераз.

Переименованиемодуля

Обычно мы используем модули с их изначальными именами, ноиногда мы должны переименовать модуль, чтобы избежать конфликтаимен. Типичной ситуацией является загрузка разных версий одного итого же модуля, например для тестирования. У модулей Lua нетвнутренней привязки к именам, поэтому обычно достаточнопереименоватьсоответствующийфайлсрасширением.lua.Однако,мыне можем отредактировать бинарную библиотеку для корректировкиимени ее функции luaopen_*. Чтобы поддерживать подобныепереименования у require есть небольшая хитрость: если имя модулясодержит дефис, то require отбрасывает этот дефис и все, что стоит

Page 194: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

передним,когдасоздаетимяфункцииluaopen_*.Например,еслимодульназываетсяа-b,тоrequireожидает,чтофункциядляегооткрытиябудетназываться luaopen_b, а не luaopen_a-b (что по-любому не являетсядопустимымименемвязыкеС).Поэтомуеслинамнужноиспользоватьдвамодулясименемmod,томыможемпереименоватьодинизнихвv1-mod, например. Когда мы вызовем m1=require "v1-mod", функция requireнайдет переименованный файл v1-mod и внутри этого файла найдетфункциюсизначальнымименемluaopen_mod.

Поискпути

При поиске файла Lua require использует путь, который несколькоотличается от типичных путей. Типичный путь — это списокдиректорий,вкоторыхнужноискатьзаданныйфайл.Однако,вANSIС(абстрактная платформа, на которой выполняется Lua) нет понятиядиректории. Поэтому путь, используемый require, — это списокшаблонов (template) каждый из которых задает свой способпреобразования имени модуля (аргумента require) в имя файла. Болееточно, каждый шаблон в пути — это имя файла, содержащеенеобязательные знаки вопроса. Для каждого шаблона require заменяеткаждый '?' на имя модуля и проверяет, есть ли файл с получившимсяименем; если нет, она переходит к следующему шаблону. Шаблоны впути разделены точками с запятой (символ, редко используемый вименах файлов в большинстве операционных систем). Например, еслипутемявляется

?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

товызовrequire"sql"попробуетоткрытьследующиефайлыLua:sql

sql.lua

c:\windows\sql

/usr/local/lua/sql/sql.lua

Функция require допускает использование лишь точки с запятой (дляразделения составляющих) и вопросительного знака; все остальное,включая разделители директорий и расширения файлов, определяетсясамимпутем.

Путь, который require использует для поиска файлов Lua, — этовсегдатекущеезначениепеременнойpackage.path.ВовремязапускаLua

Page 195: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

инициализирует эту переменную значением переменной окруженияLUA_PATH_5_2. Если эта переменная окружения не определена, LuaпытаетсяиспользоватьпеременнуюокруженияLUA_PATH.Еслиониобенеопределены, Lua использует путь по умолчанию, заданный прикомпиляции (Примечание: В Lua 5.2 опция командной строки -E

предотвращаетиспользованиеэтихпеременныхокруженияизаставляетиспользовать значение по умолчанию). При использовании значенияпеременной окружения Lua подставляет путь по умолчанию вместолюбой подстроки ";;". Например, если вы установите LUA_PATH_5_2 на"mydir/?.lua;;",тоокончательныйпутьбудетшаблоном"mydir/?.lua",закоторымследуетпутьпоумолчанию.

ПутьдляпоискабиблиотекСработаетточнотакже,ноегозначениеберетсяиз переменнойpackage.cpath (вместоpackage.path). Аналогичноэта переменная получает свое начальное значение из переменнойокруженияLUA_CPATH_5_2илиLUA_CPATH.ТипичнымзначениемэтогопутивUNIXявляетсяследующее:

./?.so;/usr/local/lib/lua/5.2/?.so

Обратите внимание, что путь определяет расширение файла.Предыдущий пример использует .so для всех шаблонов; в Windowsтипичныйпутьбудетпримернотаким:

.\?.dll;C:\ProgramFiles\Lua502\dll\?.dll

Функция package.searchpath запрограммирована с учетом всехвышеприведенных правил для поиска библиотек. Она получает имямодуляипуть,азатемищетфайл,следуяэтимправилам.Онавозвращаетлибоимяпервогонайденногофайла,либоnilисообщениеобошибке,описывающее все файлы, которые она безуспешно пыталась открыть,каквследующемпримере:

>path=".\\?.dll;C:\\ProgramFiles\\Lua502\\dll\\?.dll"

>print(package.searchpath("X",path))

nil

nofile'.\X.dll'

nofile'C:\ProgramFiles\Lua502\dll\X.dll'

Искатели

В действительности require несколько сложнее, чем мы описали.ПоискфайлаLuaибиблиотекиС—этолишьдвачастныхслучаяболее

Page 196: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

общего понятия искателя (searcher). Искатель— это просто функция,которая получает имя модуля и возвращает либо загрузчик для него,либоnil,еслинеможетнайтиниодного.

Массив package.searchers содержит перечень искателей, которымипользуется require. При поиске модуля require поочередно вызываеткаждого искателя из этого перечня, передавая ему имя модуля, до техпор, пока не найдет загрузчик для этого модуля. Если поиск не даетрезультата,requireвызываетошибку.

В Lua 5.2 параметр командной строки -Е предотвращаетиспользование переменных окружения и приводит к использованиюпути,заданногоприкомпиляции.

Использование списка для управления поиском модуля придаетогромнуюгибкостьфункцииrequire.Например,есливыхотитехранитьмодули сжатыми в zip-файлы, то вам лишь нужно предоставитьсоответствующую функцию-искатель и добавить ее к списку. Однако,чаще всего программам все же не нужно изменять содержимоеpackage.searchers. В конфигурации по умолчанию искатель файлов Luaискатель библиотек С, которые мы описали выше, занимают в спискевторуюитретьюпозиции,соответственно.Переднимистоитискательпредзагрузки.

Искатель предзагрузки (preload) позволяет определить для загрузкимодуляпроизвольнуюфункцию.Ониспользуеттаблицуpackage.preloadдля отображения имен модулей в загрузочные функции. При поискеимени модуля данный искатель просто ищет заданное имя в этойтаблице.Еслионнаходит внейфункцию, он возвращает ее в качествезагрузчика модуля. Иначе он возвращает nil. Этот искательпредоставляетобщийметоддляобработкинекоторыхнетрадиционныхситуаций. Например, библиотека С, статически прилинкованная к Lua,может зарегистрировать своюфункциюluaopen_ в таблицеpreload так,чтоонабудетвызвана,толькокогда(иесли)пользователюпонадобитсяэтот модуль. Таким образом, программа не тратит время на открытиемодуля,еслионнеиспользуется.

По умолчанию package.searchers включает в себя четвертуюфункцию, которая нужна лишь для подмодулей. Мы обсудим ее вразделе15.4.

15.2.ОсновнойподходкнаписаниюмодулейнаLua

Простейший способ создать модуль на Lua поистине прост: мы

Page 197: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

создаем таблицу, помещаем все функции, которые мы хотимэкспортировать, внутрь нее и возвращаем эту таблицу. Листинг 15.1демонстрирует этот подход. Обратите внимание на то, как мыопределяем функцию inv в качестве закрытой, просто объявляя еелокальнойдляэтогокуска.

Листинг15.1.ПростоймодульдлякомплексныхчиселlocalM={}

functionM.new(r,i)return{r=r,i=i}end

--определяетконстанту'i'

M.i=M.new(0,1)

functionM.add(c1,c2)

returnM.new(c1.r+c2.r,c1.i+c2.i)

end

functionM.sub(c1,c2)

returnM.new(c1.r-c2.r,c1.i-c2.i)

end

functionM.mul(c1,c2)

returnM.new(c1.r*c2.r-c1.i*c2.i,c1.r*c2.i+c1.i*c2.r)

end

localfunctioninv(c)

localn=c.r^2+c.i^2

returnM.new(c.r/n,-c.i/n)

end

functionM.div(c1,c2)

returnM.mul(c1,inv(c2))

end

functionM.tostring(c)

return"("..c.r..","..c.i..")"

end

returnM

Некоторымнеправитсяоператорreturnвконце.Однимизспособовегоустраненияявляетсяприсваиваниетаблицымодулянепосредственноpackage.loaded:

localM={}

package.loaded[...]=M

<какпрежде>

Page 198: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Вспомним, что require вызывает загрузчик, передавая имя модуля какпервыйаргумент.Поэтомувыражениеспеременнымчисломаргументов... в индексе приводит к возврату этого имени. После этогоприсваиваниянамбольшененужновозвращатьM в концемодуля: еслимодуль не возвращает значение, то require вернет текущее значениеpackage.loaded[modname] (если оно не nil). В любом случае, япредпочитаю писать return в конце модуля, поскольку это выглядитаккуратнее.

Другойспособ записимодулясостоитвопределениивсехфункцийкаклокальныхипостроениивозвращаемойтаблицывконцемодуля,какв листинге 15.2. В чем преимущества этого подхода? Вам не нужноначинать каждое имя с M. или чего-то похожего; здесь явный списокэкспортируемых функций; вы определяете и используетеэкспортируемыеивнутренниефункциивнутримодуляоднимитемжеобразом. В чем недостатки этого подхода? Список экспортируемыхфункцийнаходитсявконцемодуля,аневначале,гдеонбылбыболееудобенвкачествебыстройсправки;иэтотсписоквнекоторойстепениизбыточен,таккаккаждоеимянужнописатьдважды.(Этотпоследнийнедостаток может стать преимуществом, поскольку позволяетфункциям иметь разные имена снаружи модуля и внутри него, но ядумаю,чтопрограммистыредкоэтимпользуются.)Личномненравитсяданныйстиль,новкусыувсехразные.

Листинг 15.2. Модуль со списком экспортируемыхфункций

localfunctionnew(r,i)return{r=r,i=i}end

--определяетконстанту'i'

locali=complex.new(0,1)

<другиефункцииследуютэтомужеобразцу>

return{

new=new,

i=i,

add=add,

sub=sub,

mul=mul,

div=div,

tostring=tostring,

}

Page 199: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

В любом случае, помните, что вне зависимости от того, какопределен модуль, пользователи должны иметь возможностьиспользоватьегостандартнымобразом:

localcpx=require"complex"

print(cpx.tostring(cpx.add(cpx.new(3,4),cpx.i)))

-->(3,5)

15.3.Использованиеокружений

Одним из недостатков тех базовых методов для создания модулейявляется то, что с ними слишком легко засорить глобальноепространствоимен,напримерпростозабывlocalвзакрытомобъявлении.

Окруженияпредоставляютинтересныйподходксозданиюмодулей,которыйрешаетэтупроблему.Когдауглавногокускамодуляестьсвоеокружение,товэтутаблицупереходятнетольковсеегофункции,ноивсеглобальныепеременные.Поэтомумыможемобъявитьвсеоткрытыефункции в качестве глобальных переменных, и они автоматическипопадут в отдельную таблицу. Все, что нужно сделать модулю, —присвоитьэтутаблицупеременной_ENV.Послеэтого,когдамыобъявимфункциюadd,онастанетM.add:

localM={}

_ENV=M

functionadd(c1,c2)

returnnew(c1.r+c2.r,c1.i+c2.i)

end

Болеетого,мыможемвызыватьдругиефункцииизэтогожемодулябезкакого-либо префикса. В предыдущем коде add обращается к new изсвоегоокружения,тоестьвызываетM.new.

Этот метод обеспечивает хорошую поддержку модулей, требуяоченьнебольшойработыотпрограммиста.Префиксыснимвообщененужны. Нет никакой разницы между вызовом экспортированной изакрытойфункций.Еслипрограммистзабываетвставить local,тооннезасоряетглобальноепространствоимен;вместоэтогозакрытаяфункцияпростостановитсяоткрытой.

Темнеменее, внастоящеевремяяпо-прежнемупредпочитаюодиниздвухметодов,рассмотренныхвпредыдущемразделе.Хотяонимогутпотребовать чуть больше работы, результат выполнения такого кодаболеепонятен.Чтобынесоздатьпоошибкеглобальнуюпеременную,япользуюсь простым методом, который состоит в присваивании _ENV

Page 200: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

значения nil. После этого любое присваивание глобальной переменнойвызоветошибку.

Чего при этом не хватает, так это, разумеется, доступа к другиммодулям.Кактолькомыизменимзначение_ENV,мыпотеряемдоступковсем предыдущим глобальным переменным. Есть несколько способоввосстановитьэтотдоступ,каждыйсосвоимиплюсамииминусами.

Первыйспособсостоитвприменениинаследования:localM={}

setmetatable(M,{__index=_G})

_ENV=M

(Вамнужновызватьsetmetatableпередприсваиванием_ENV;зачем?)Сданной конструкцией у модуля есть прямой доступ к любомуглобальному идентификатору, с небольшими затратами при каждомобращении.Любопытнымпоследствиемэтогорешенияявляетсято,чтоваш модуль, по идее, теперь содержит все глобальные переменные.Например, любой, кто использует вашмодуль, теперьможет вызыватьстандартную функцию для вычисления синуса, написавcomplex.math.sin(х). (ДаннаяособенностьтакжеестьвсистемепакетовPerl.)

Еще одним быстрым методом доступа к другим модулям являетсяобъявлениелокальнойпеременной,котораяхранитисходноеокружение:

localM={}

local_G=_G

_ENV=M--или_ENV=nil

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

Более взвешенный подход заключается в том, чтобы объявитьлокальными только те функции, которые вам нужны, или, по крайнеймере,лишьнужныеваммодули:

--настройкамодуля

localM={}

--Импортируемаясекция:

--объявляетвсе,чтоэтомумодулюпотребуетсяизвне

localsqrt=math.sqrt

localio=io

--сданногомоментавнешнегодоступабольшенет

_ENV=nil--or_ENV=M

Page 201: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

15.4.Подмодулиипакеты

Lua разрешает именам модулей быть иерархическими, используяточкудляразделенияуровнейимен.Например,модульсименемmod.subявляетсяподмодулеммодуляmod.Пакет— это полное дерево модулей;онявляетсяединицейраспространениякодавLua.

Когда вам нужен модуль с именем mod.sub, функция require сперваобращаетсяктаблицеpackage.loaded, а затемк таблицеpackage.preload,используяполноеимя "mod.sub" в качестве ключа; в этом случае точкаявляетсятакимжесимволомвименемодуля,какилюбойдругой.

Однако,припоискефайла,определяющегоэтотподмодуль,requireпереводит точку в другой символ, обычно системный разделительдиректорий (то есть '/' для UNIX и '\' для Windows). После этогопреобразования require ищет получившееся имя, как и любое другое.Предположим,чтоунасестьразделительдиректорий '/' и следующийпуть:

./?.lua;/usr/local/lua/?.lua;/usr/local/lua/?/init.lua

Вызовrequire"a.b"попробуетоткрытьследующиефайлы:./a/b.lua

/usr/local/lua/a/b.lua

/usr/local/lua/a/b/init.lua

Это поведение позволяет всем модулям пакета находиться в однойдиректории.Например,есливпакетесодержатсямодулир,р.аир.b,тосоответствующими файлами могут быть p/init.lua, р/а.lua и p/b.lua,директорияркоторыхсодержитсявдругойподходящейдиректории.

Разделительдиректорий,используемыйLua,настраиваетсявовремякомпиляциииможетбытьлюбой строкой (вспомните, чтоLuaничегоне знает про директории). Например, системы без иерархическихдиректорий могут использовать в качестве такого разделителя '_', такчтоrequire"a.b"будетискатьфайлa_b.lua.

Имена в С не могут содержать точки, поэтому библиотека С дляподмодуля а.b не может экспортировать функцию luaopen_a.b. В этом

Page 202: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

случае require переводит точку в другой символ — подчеркивание.Таким образом, библиотека С с именем а.b должна назвать своюинициализирующую функцию luaopen_a_b. Мы также можемвоспользоваться здесь уловкой с дефисом, что может пригодиться внекоторыхслучаях.Например,еслиунасестьбиблиотекаСсименемаимы хотим сделать ее подмодулем mod, то мы можем переименовать еефайл в mod/v-a. Когда мы напишем require "mod.v-a", функция requireправильнонайдетновыйфайлmod/v-a, такжекакифункциюluaopen_aвнутринего.

В качестве дополнительного средства у require есть еще одинискательдлязагрузкиподмодулейС.КогдаоннеможетнайтинифайлLua,нифайлСдляподмодуля,этотискательопятьищетвпутидляС,нонаэтотразонищетимяпакета.Например,еслипрограмметребуетсяподмодуль а.b.с, этот искатель будет искать а. Если он найдетбиблиотекуСсэтимименем,тоrequireбудетискатьвэтойбиблиотекесоответствующую открывающую функцию, в нашем случаеluaopen_a_b_c. Данное средство позволяет размещать несколькоподмодулей в одной библиотеке С, каждый со своей открывающейфункцией.

СточкизренияLua,подмодуливодномпакетенеимеютявнойсвязи.Загрузкамодуляанеприводиткавтоматическойзагрузкелюбогоизееподмодулей; аналогично, при загрузке a.b не произойдетавтоматической загрузки а. Конечно, разработчик пакета при желанииможет задать эти связи. Например, модуль а может при загрузке явнопотребоватьодиниливсесвоиподмодули.

Упражнения

Упражнение 15.1. Перепишите код в листинге 13.1 в видесоответственногомодуля.

Упражнение15.2.Что случитсяприпоискебиблиотеки, есликакая-точастьпутизафиксирована(тоестьнесодержитзнаквопроса)?Можетлипригодитьсятакоеповедение?

Упражнение15.3.Напишитеискатель, которыйодновременноищетфайлыLua и библиотекиС. Например, путь для этого искателя можетбытьчем-товроде:

./?.lua;./?.so;/usr/lib/lua5.2/?.so;/usr/share/lua5.2/?.lua

Page 203: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

(Подсказка: используйте package.searchpath для поискасоответствующего файла и потом попытайтесь загрузить его, сначалаприпомощиloadfile,азатемприпомощиpackage.loadlib.)

Упражнение15.4.Чтослучится,есливыустановитеметатаблицудляpackage.preloadприпомощиметаметода__index?Можетлипригодитьсятакоеповедение?

Page 204: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА16

Объектно-ориентированноепрограммирование

Таблица в Lua — это объект во многих отношениях. Подобнообъектам, у таблиц есть состояние. Подобно объектам, у таблиц естьидентичность (собственное «я» — self), которая не зависит от еезначений; в частности, две таблицы с одинаковыми значениямиявляютсяразнымиобъектами,ипри этомкаждыйобъектможетиметьразныезначениявразныемоментывремени.Подобнообъектам,утаблицестьжизненныйцикл,которыйнезависитоттого,ктоихсоздалилигдеонибылисозданы.

Уобъектовестьсвоисобственныедействия.Утаблицтакжемогутбытьдействия,какпоказанониже:

Account={balance=0}

functionAccount.withdraw(v)

Account.balance=Account.balance-v

end

Это определение создает новую функцию и хранит ее в поле withdrawобъектаAccount.Затеммыможемвызватьее,какпоказанониже:

Account.withdraw(100.00)

Данная разновидность функции— это почти то, что мы называемметодом. Однако, использование глобального имени Account внутрифункцииявляетсяплохойпрактикойпрограммирования.Во-первых,этафункция будет работать только для данного конкретного объекта.Во-вторых, дажедля этого объектафункциябудет работать ровнодо техпор, пока этот объект хранится в той конкретной глобальнойпеременной.Еслимыизменимимяобъекта,тоwithdrawбольшенебудетработать:

a,Account=Account,nil

a.withdraw(100.00)--ОШИБКА!

Подобноеповедениенарушаетпринцип,чтоукаждогообъектадолженбытьсвой,независимыйциклжизни.

Более гибкий подход состоит в работе с получателем (receiver)операции. Для этого нашему методу понадобится дополнительный

Page 205: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

параметр со значением получателя. Этот параметр обычно называетсяselfилиthis:

functionAccount.withdraw(self,v)

self.balance=self.balance-v

end

Теперь при вызове метода мы должны задать объект, над которым ондолженработать:

a1=Account;Account=nil

...

a1.withdraw(a1,100.00)--OK

Прииспользованиипараметраselfмыможемиспользоватьодинитотжеметоддлямногихобъектов:

a2={balance=0,withdraw=Account.withdraw}

...

a2.withdraw(a2,260.00)

Данное применение параметра self является ключевым моментом влюбом объектно-ориентированном языке. В большинстве объектно-ориентированных языков данный механизм частично скрыт отпрограммиста, поэтому этот параметр не нужно объявлять (хотя онможно по-прежнему пользоваться именем self или this внутри метода).Lua также может скрывать этот параметр при помощи операциидвоеточия. Мы можем переписать предыдущее определение метода ввиде

functionAccount:withdraw(v)

self.balance=self.balance-v

end

авызовметодакакa:withdraw(100.00)

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

Page 206: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Account={balance=0,

withdraw=function(self,v)

self.balance=self.balance-v

end

}

functionAccount:deposit(v)

self.balance=self.balance+v

end

Account.deposit(Account,200.00)

Account:withdraw(100.00)

Кданномумоментуунашихобъектовестьидентичность,состояниеи действия над этим состоянием.Им по-прежнему не хватает системыклассов, наследования и скрытия членов. Давайте займемся первойзадачей:какнамсоздатьразличныеобъектысодинаковымповедением?Вчастности,какнамсоздатьнесколькосчетов?

16.1.Классы

Класс работает как шаблон для создания объектов. Большинствообъектно-ориентированных языков предлагает понятие класса. В такихязыках каждый объект является экземпляром какого-то конкретногокласса. В Lua нет понятия класса; каждый объект определяет своесобственное поведение и состояние. Однако, смоделировать в Luaклассынетрудно,еслиследоватьпримеруязыковнаосновепрототипов,вроде Self или NewtonScript. В этих языках у объектов нет классов.Вместо этого у каждого объекта может быть прототип, которыйявляется обычным объектом, в котором первый объект ищетнеизвестные ему действия. Для представления классов в таких языкахмы просто создаем объект, который будет использован только вкачествепрототипадлядругихобъектов(егоэкземпляров).Иклассы,ипрототипыработаютв качествеместхраненияповедения, общегодляразличныхобъектов.

В Lua мы можем реализовать прототипы, используя идеюнаследованияизраздела13.4.Точнее,еслиунасестьдваобъектааиb,товсе,чтонамнужносделать,чтобыbсталпрототипомдляа,—этоследующее:

setmetatable(a,{__index=b})

Послеэтогоабудетискатьвbлюбоедействие,которогоунет.Называть

Page 207: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

объектbклассомобъектаа—этонечтоиное,какзаменатерминов.Давайте вернемся к нашему примеру с банковским счетом. Для

созданиядругихсчетовсповедением,аналогичнымAccount,мысделаемтак, что эти новые объекты унаследуют свои действия от Account припомощиметаметода__index.Небольшая оптимизация будет состоять втом, что намне нужно создавать дополнительную таблицу в качествеметатаблицы для объектов Account; для этой цели мы будемиспользоватьсамутаблицуAccount:

functionAccount:new(o)

o=oor{}--создаеттаблицу,еслипользовательеене

предоставил

setmetatable(o,self)

self.__index=self

returno

end

(Когда мы вызываем Account:new, параметр self равен Account;поэтомумымогли бы явно использовать Account вместо self. Однако,использованиеselfотличнопригодитсявследующемразделе,когдамывведемнаследование.)Чтопроизойдетпослевыполненияданногокода,когдамысоздадимновыйсчетивызовемегометод,какпоказанониже?

a=Account:new{balance=0}

a:deposit(100.00)

Когдамысоздаемновыйсчет,уавкачествеметатаблицыбудетAccount(из-за параметра self в вызове Account:new). Затем, когда мы вызываема:deposit(100.00), на самомделепроисходитвызовa.deposit(а,100.00);двоеточие — это всего лишь синтаксический сахар. Однако, Lua неможет найти запись "deposit" в таблице а; поэтому Lua ищет запись__indexвметатаблице.Теперьситуациявыглядитпримерноследующимобразом:

getmetatable(a).__index.deposit(a,100.00)

Метатаблицейа является Account, а Account.__index— это тоже Account(поскольку метод new выполнил self.__index=self). Поэтомупредыдущеевыражениесокращаетсядо

Account.deposit(a,100.00)

То есть Lua вызывает исходную функцию deposit, но передает ей а вкачестве параметра self. Таким образом, новый счет а унаследовал

Page 208: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

функциюdepositотAccount.ПоэтойжесхемеоннаследуетвсеполяотAccount.

Это наследование работает не только дляметодов, но также и длядругих полей, отсутствующих в новом счете. Поэтому класс можетпредоставлять не только методы, но и значения по умолчанию дляполейэкземпляра.Напомним,чтовнашемпервомопределении Accountмы предоставили поле balance со значением 0. Поэтому если мысоздадим счет без начального значения баланса, то он унаследует этозначениепоумолчанию:

b=Account:new()

print(b.balance)-->0

При вызове метода deposit у b он выполнит код, эквивалентныйследующему(посколькуselfравенb):

b.balance=b.balance+v

Выражение b.balance дает 0, и метод присваивает начальный вкладb.balance. Последующие обращения к b.balance уже не приведут квызову метаметода __index, так как у b теперь есть свое собственноеполеbalance.

16.2.Наследование

Поскольку классы являются объектами, они также могут получатьметодыотдругихклассов.Этоповедениепозволяетлегкореализоватьнаследование(вобычномобъектно-ориентированномсмысле)вLua.

Пустьунасестьбазовыйкласс,такойкакAccount:Account={balance=0}

functionAccount:new(o)

o=oor{}

setmetatable(o,self)

self.__index=self

returno

end

functionAccount:deposit(v)

self.balance=self.balance+v

end

functionAccount:withdraw(v)

ifv>self.balancethenerror"insufficientfunds"end

Page 209: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

self.balance=self.balance-v

end

От этого класса мы можем унаследовать подкласс SpecialAccount,позволяющий покупателю снять больше, чем есть на его балансе.Мыначинаемспустогокласса,которыйпростонаследуетвсесвоиоперацииотсвоегобазовогокласса:

SpecialAccount=Account:new()

ДосихпорSpecialAccountявляетсялишьоднимизэкземпляровAccount.Теперьпроисходитзамечательнаявещь:

s=SpecialAccount:new{limit=1000.00}

SpecialAccount наследует new от Account, как и любые другие методы.Однако, на этот раз при выполнении new его параметр self уже будетссылаться на SpecialAccount. Поэтому метатаблицей s будетSpecialAccount, чье значение в поле __index тоже равно SpecialAccount.Поэтому s наследует от SpecialAccount, который, в свою очередь,наследуетотAccount.Привычислении

s:deposit(100.00)

Lua не сможет найти поле deposit в s, поэтому он будет искать его вSpecialAccount; там его он тоже не найдет и потому поищет в Account,гдеиобнаружитисходнуюреализациюэтогометода.

Что делает SpecialAccount особенным, так это то, что мы можемпереопределитьлюбойметод,унаследованныйотегосуперкласса.Все,чтонамнужно,—этопростозаписатьновыйметод:

functionSpecialAccount:withdraw(v)

ifv-self.balance>=self:getLimit()then

error"insufficientfunds"

end

self.balance=self.balance-v

end

functionSpecialAccount:getLimit()

returnself.limitor0

end

Теперь,когдамывызовемs:withdraw(200.00),LuaнеобратитсякAccount,поскольку первым он найдет новый метод withdraw в классеSpecialAccount.Таккакs.limitравно1000.00(каквыпомните,мызадалиэто поле при создании s), то программа осуществит снятие со счета,

Page 210: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

оставляяврезультатеsсотрицательнымбалансом.У объектов в Lua есть интересный аспект: вам не нужно создавать

новый класс для задания нового поведения. Если требуется изменитьповедение лишь одного объекта, то мы можем реализовать данноеповедение непосредственно в этом объекте. Например, если счет sпредставляетособогоклиента,чейлимитвсегдаравен10%оттекущегобаланса,томыможемизменитьлишьэтотодинсчет:

functions:getLimit()

returnself.balance*0.10

end

После данного объявления вызов s:withdraw(200.0) выполнит методwithdraw из класса SpecialAccount, но когда withdraw вызоветself:getLimit,произойдетвызовтолькочтоопределеннойфункции.

16.3.Множественноенаследование

Поскольку в Lua объекты не являются примитивами, в нем естьнесколько способов реализации объектно-ориентированногопрограммирования.Подходсприменениемметаметода__index,которыймы только что видели, является, наверное, лучшей комбинациейпростоты, скорости и гибкости. Однако, есть и другие реализации,которые могут оказаться более подходящими в некоторых частныхслучаях. Ниже мы увидим альтернативную реализацию, котораяобеспечиваетмножественноенаследованиевLua.

Основой данной реализации является использование функции дляметаполя__index.Вспомнимотом,чтокогдауметатаблицынекоторойтаблицыестьфункциявполе__index,Luaбудетвызыватьэтуфункциювсякийраз, когдане сможетнайтиключв этойисходнойтаблице.Приэтом __index может искать отсутствующий ключ в любом количестверодительскихклассов.

Множественное наследование означает, что у класса может бытьболее одного суперкласса. Таким образом, мы не можем использоватьметод класса для создания подклассов. Взамен с этой целью мыопределим особую функцию createClass, у которой в качествеаргументов суперклассы нового класса (см. листинг 16.1). Этафункциясоздает таблицу для представления нового класса и устанавливает егометатаблицу с метаметодом __index, который и реализуетмножественное наследование. Несмотря на множественное

Page 211: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

Листинг16.1.Реализациямножественногонаследования--ищет'k'вспискетаблиц'plist'

localfunctionsearch(k,plist)

fori=1,#plistdo

localv=plist[i][k]--пробует'i'-ыйсуперкласс

ifvthenreturnvend

end

end

functioncreateClass(...)

localc={}--новыйкласс

localparents={...}

--классбудетискатькаждыйметодвспискесвоихродительских

классов

setmetatable(c,{__index=function(t,k)

returnsearch(k,parents)

end})

--подготавливает'c'статьметатаблицейдлясвоихэкземпляров

c.__index=c

--определяетновыйконструктордляэтогоновогокласса

functionc:new(o)

o=oor{}

setmetatable(o,c)

returno

end

returnc--возвращаетновыйкласс

end

Давайте проиллюстрируем использование createClass при помощинебольшогопримера.ПустьунасестьнашпредыдущийклассAccountиновыйклассNamedлишьсдвумяметодами:setnameиgetname.

Named={}

functionNamed:getname()

returnself.name

Page 212: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

end

functionNamed:setname(n)

self.name=n

end

ДлясозданияновогоклассаNamedAccount, которыйявляетсяподклассомиAccount,иNamed,мыпростовызовемcreateClass:

NamedAccount=createClass(Account,Named)

Мысоздаемииспользуемэкземплярыэтогоклассакакобычно:account=NamedAccount:new{name="Paul"}

print(account:getname())-->Paul

Теперьдавайтепроследим,какработаетэтотпоследнийоператор.Luaнеможетнайтиполе"getname"вaccount;поэтомуонищетполе__indexвметатаблицеaccount,тоестьвNamedAccount.НовNamedAccountтакженетполя "getname", поэтому Lua ищет поле __index в метатаблицеNamedAccount. Поскольку это поле содержитфункцию, Lua вызывает ее.Далееэтафункциясперваищет"getname"вAccountи,необнаруживего,проверяетNamed, гдеонаинаходитотличноеотnilзначение,котороеистановитсяокончательнымрезультатомпоиска.

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

setmetatable(c,{__index=function(t,k)

localv=search(k,parents)

t[k]=v--сохраняетдляследующегообращения

returnv

end})

При помощи данного приема доступ к унаследованным методамстановится стольже быстрым, как и доступ к локальнымметодам (заисключением первого обращения). Недостаток состоит в том, что вовремя выполнения сложно изменить определения методов, посколькуэтиизменениянепередаютсяпоцепочкенаследования.

16.4.Конфиденциальность

Page 213: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Многие считают конфиденциальность (возможность скрытияэлементов) неотъемлемой частью объектно-ориентированного языка;состояние каждого объекта является его личным делом. В некоторыхобъектно-ориентированных языках, таких как C++ и Java, вы можетеуправлять тем, будет ли поле объекта (также называемое экземплярнойпеременной) или его метод видны вне этого объекта. Язык Smalltalk,который популяризовал объектно-ориентированные языки, делает всепеременные закрытыми (private), а все методы открытыми (public).Simula, самый первый объектно-ориентированный язык, вообще необеспечивалзащитуданных.

ОсновнаясхемаработысобъектамивLua,которуюмырассмотрелидоэтого,непредоставляетмеханизмыскрытия.Частичноэтоявляетсяследствием нашего применения общих структур (таблиц) дляпредставления объектов. Кроме того, Lua избегает чрезмернойизбыточности и искусственных ограничений. Если вы не хотитеобращатьсякчему-либовнутриобъекта,топростонеделайтеэтого.

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

Основнаяидеяэтойальтернативнойсхемы—представлятькаждыйобъектприпомощидвухтаблиц:одна—дляегосостояния,адругая—для его действий (его интерфейс). Обращение к самому объектупроисходит через вторую таблицу, то есть посредством действий,образующих его интерфейс. Во избежание несанкционированногодоступа, таблица, представляющая состояние объекта, не хранится вполе другой таблицы; вместо этого она хранится лишь в замыканииметодов. Например, применив данную схему для представлениябанковскогосчета,мымоглибысоздаватьновыеобъектыприпомощиследующейфункции-фабрики:

functionnewAccount(initialBalance)

localself={balance=initialBalance}

localwithdraw=function(v)

self.balance=self.balance-v

end

Page 214: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localdeposit=function(v)

self.balance=self.balance+v

end

localgetBalance=function()returnself.balanceend

return{

withdraw=withdraw,

deposit=deposit,

getBalance=getBalance

}

end

Сначала функция создает таблицу для содержания внутреннегосостояния объекта и хранит ее в локальной переменной self. Затемфункция создает методы этого объекта. Наконец, функция создает ивозвращаетвнешнийобъект,которыйотображаетименаметодовнаихнастоящие реализации.Ключевоймомент здесь в том, что этиметодыне получают self как дополнительный параметр; вместо этого ониобращаются к self напрямую. Поскольку дополнительного аргументанет, мы не используем синтаксис с двоеточием для работы с такимиобъектами.Мывызываемихметодыпростокакобычныефункции:

acc1=newAccount(100.00)

acc1.withdraw(40.00)

print(acc1.getBalance())-->60

Эта схема обеспечивает полную конфиденциальность всего, чтохранится в таблице self. После возврата управления из функцииnewAccount нет никакого способа получить прямой доступ к этойтаблице. Хотя наш пример помещает в таблицу скрытия лишь однуэкземплярную переменную, мы можем хранить в этой таблице всезакрытыечастиобъекта.всегооднупеременнуювзакрытойтаблице,мыможем хранить все закрытые части объекта в этой таблице. Такимжеобразом мы можем определить и закрытые методы: они похожи наоткрытые, но мы не помещаем их в интерфейс. Например, наши счетамогли бы предоставлять дополнительный 10%-ый кредит темпользователям,чейбалансужепревысилопределенныйлимит,нонамненужно, чтобы у пользователей был доступ к деталям таких расчетов.Мыможемреализоватьэтуфункциональностьследующимобразом:

functionnewAccount(initialBalance)

localself={

balance=initialBalance,

Page 215: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

LIM=10000.00,

}

localextra=function()

ifself.balance>self.LIMthen

returnself.balance*0.10

else

return0

end

end

localgetBalance=function()

returnself.balance+extra()

end

<какпрежде>

Опятьже,ниодинпользовательникоимобразомнеможетобратитьсякextraнапрямую.

16.5.Подходсединственнымметодом

Частным случаем предыдущего подхода для объектно-ориентированного программирования является случай, когда у объектаесть лишь один метод. В подобном случае нам не нужно создаватьинтерфейсную таблицу;мыможем просто вернуть этот единственныйметод в качестве представления объекта. Если это звучит немногостранно, то стоит перечитать раздел 7.1, где мы конструировалиитерирующие функции, хранящие свое состояние как замыкания.Итератор, хранящий свое состояние, — это ничто иное как объект соднимметодом.

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

functionnewObject(value)

returnfunction(action,v)

ifaction=="get"thenreturnvalue

elseifaction=="set"thenvalue=v

elseerror("invalidaction")

end

end

end

Егоиспользованиеневызываетзатруднений:

Page 216: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

d=newObject(0)

print(d("get"))-->0

d("set",10)

print(d("get"))-->10

Такаянеобычнаяреализацияобъектоввесьмаэффективна.Синтаксисd("set",10)хотяивыглядитстранно,всегонадвасимволадлиннее,чемболее традиционный d:set(10). Каждый объект использует одноединственное замыкание, что дешевле одной таблицы. Здесь нетнаследования, но зато у нас есть полная конфиденциальность:обратитьсяксостояниюобъектаможнолишьоднимспособом—черезегоединственныйметод.

Tcl/Tkиспользуетсхожийподходдлясвоихвиджетов.ИмявиджетавTkобозначаетфункцию(командувиджета),котораяможетвыполнятьвсевидыдействийнадвиджетом.

Упражнения

Упражнение16.1.РеализуйтеклассStack сметодамиpush,pop,top иisempty.

Упражнение 16.2. Реализуйте класс StackQueue как подкласс Stack.Кроме унаследованных методов, добавьте к этому классу методinsertbottom, который вставляет элемент в конец стека. (Этот методпозволяетиспользоватьобъектыданногоклассакакочереди.)

Упражнение 16.3. Другой способ обеспечить конфиденциальностьобъектов — это реализовать их через посредников (см. раздел 13.4).Каждыйобъектпредставленпустойтаблицей-посредником.Внутренняятаблица отображает посредников на таблицы, хранящие состояниеобъекта. Эта внутренняя таблица не доступна снаружи, но методыиспользуютеедляпереводасвоегопараметраselfвреальнуютаблицу,скоторойониработают.РеализуйтепримерсклассомAccountприпомощиэтогоподходаирассмотритеегоплюсыиминусы.

(С этим подходом есть одна маленькая проблема. Постарайтесьнайти ее сами или обратитесь к разделу 17.3, где предлагается еерешение.)

Page 217: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА17

Слабыетаблицыифинализаторы

Lua осуществляет автоматическое управление памятью.Программысоздают объекты (таблицы, нити и т. п.), но функции уничтоженияобъектов не существует. Lua автоматически уничтожает объекты,которые становятся мусором, при помощи сборки мусора. Этоосвобождает вас от бремени управления памятью и, что более важно,освобождаетотбольшинстваошибок,связанныхсэтойдеятельностью,такихкакповисшиеуказателииутечкипамяти.

Применение сборщика мусора означает, что у Lua нет проблем сциклами.Вамненужнопредприниматьникакихспециальныхдействийприиспользованиициклических структурданных; ониосвобождаютсяавтоматически,какилюбыедругиеданные.Темнеменее,иногдадажеумномусборщикумусоранужнавашапомощь.Ниодинсборщикмусоранепозволитвамзабытьобовсехпроблемахуправленияресурсами,такихкакпереполнениепамятиивнешниересурсы.

Слабые таблицы и финализаторы — это механизмы, которые выможете использовать в Lua, чтобы помочь сборщику мусора. Слабыетаблицы позволяют сбор объектов Lua, которые все еще доступныпрограмме, в то время как финализаторы позволяют сборку внешнихобъектов,ненаходящихсяподнепосредственнымконтролемсборщикамусора.Вэтойглавемыобсудимобаэтихмеханизма.

17.1.Слабыетаблицы

Сборщик мусора может собрать только то, что гарантированноявляется мусором; он не может догадаться, что считаете мусоромименно вы. Типичным примером является стек, реализованный какмассивсиндексомдлявершиныстека.Вызнаете,чтодопустималишьтачастьмассива,котораяидетдовершины,ноэтогонезнаетLua.Есливы выталкиваете элемент, просто уменьшая индекс вершины, тооставшийсявмассивеобъектнеявляетсямусоромдляLua.Точнотакжелюбой объект, который хранится в глобальной переменной, не будетмусоромдляLua,дажеесливашапрограмманикогданевоспользуетсяим снова. В обоих случаях вам (т.е. вашей программе) придется

Page 218: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

позаботиться о присваивании nil этим позициям, чтобы они вдальнейшемнепомешалиуничтожениюданногообъекта.

Темнеменее, простой чистки ваших ссылок не всегда достаточно.Для некоторых конструкций нужно организовать дополнительноевзаимодействие между программой и сборщиком мусора. Типичнымпримеромявляетсяхранениеввашейпрограммеколлекциивсехживыхобъектов определенного вида (например, файлов). Задача кажетсяпростой: вам лишь требуется вставлять каждый новый объект вколлекцию. Однако, как только объект становится частью этойколлекции, его уничтожение становится невозможным! Даже если нанего не ссылаются другие объекты, на него ссылается сама коллекция.Lua не может знать о том, что эта ссылка не должна препятствоватьутилизацииданногообъекта,еслитольковынесообщилиLuaэтотфакт.

Слабые таблицы — это тот механизм, который вы используете,чтобы указать Lua на то, что ссылка не должна препятствоватьуничтожению объекта. Слабая ссылка (weak reference) — это такаяссылканаобъект, котораянеучитывается сборщикоммусора.Есливсессылки, указывающие на объект, являются слабыми, то данный объектутилизируется,аэтислабыессылкикаким-либообразомудаляются.Luaреализует слабые ссылки как слабые таблицы: слабая таблица (weaktable)—этотакаятаблица,всессылкикоторойявляютсяслабыми.Этозначит,чтоеслиобъектхранитсятольковнутрислабыхтаблиц,тоLuaсовременемегоутилизирует.

Утаблицестьключиизначения,которыеприэтоммогутсодержатьлюбыевидыобъектов.Вобычныхобстоятельствахсборщикмусоранеутилизирует объекты, которые являются ключами и ссылками воткрытой для доступа таблице. То есть и ключи, и значения являютсясильными ссылками (strong reference), то есть они предотвращаютутилизациютехобъектов,накоторыеониуказывают.Вслабойтаблицеиключи,и значениямогутбытьслабыми.Это значит,что существуюттри вида слабых таблиц: таблицы со слабыми ключами, таблицы сослабыми значениями и полностью слабые таблицы, где и ключи, изначения являются слабыми. Независимо от вида таблицы, приуничтоженииключаилизначенияизнееудаляетсявсязапись.

Слабость таблицы задается полем __mode ее метатаблицы. Значениеэтого поля, когда оно присутствует, должно быть строкой; если этастрокаравна"k", тоключив этойтаблицеявляютсяслабыми;еслиэтастрокаравна"v",тослабымиявляютсязначениявэтойтаблице;еслиэтастрока равна "kv", то и ключи, и значения в данной таблице являются

Page 219: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

слабыми. Следующий пример, хотя и искусственный, показываетосновноеповедениеслабыхтаблиц:

a={}

b={__mode="k"}

setmetatable(a,b)--теперьу'a'естьслабыеключи

key={}--создаетпервыйключ

a[key]=1

key={}--создаетвторойключ

a[key]=2

collectgarbage()--принудительнозадействуетциклсборки

мусора

fork,vinpairs(a)doprint(v)end

-->2

В этом примере второе присваивание kеу={} перезаписывает ссылку напервыйключ.Вызовcollectgarbageзаставляетсборщикмусорапровестиполную утилизацию. Поскольку на первый ключ больше не осталосьссылок, этотключутилизируется, а соответствующая записьв таблицеудаляется.Однаковторойключпо-прежнемухранитсявпеременнойkeyипоэтомунеутилизируется.

Обратите внимание, что из слабой таблицы могут бытьутилизированы лишь объекты. Значения, например числа и логическиезначения, сборщиком не собираются. Например, если мы вставимчисловой ключ в таблицу а (из нашего предыдущего примера), тосборщик мусора никогда его не удалит. Разумеется, если значение,соответствующее числовому ключу, хранится в таблице со слабымизначениями,тоизнееудаляетсявсясоответствующаязапись.

Со строками есть одна тонкость: хотя строки и утилизируютсясборщиком мусора, с точки зрения реализации они отличаются отостальныхутилизируемыхобъектов.Другиеобъекты,такиекактаблицыинити, создаются явно.Например, когдаLua вычисляет выражение{},тоонсоздаетновуютаблицу.Однако,создаетлиLuaновуюстрокупривычислении"a".."b"?Что,есливсистемеужеестьстрока"ab"?СоздастлиLua новую строку?Может ли компилятор создать эту строку передвыполнением программы? Это не имеет никакого значения: это вседетали реализации. С точки зрения программиста, строки являютсязначениями,анеобъектами.Поэтому,такжекакичислоилилогическоезначение, строка не удаляется из слабой таблицы (кроме случая, когдаудаляетсясвязанноеснейзначение).

17.2.Функциисзапоминанием

Page 220: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Распространенным программистским приемом является получениевыигрыша во времени за счет проигрыша по памяти. Вы можетеускорить функцию посредством запоминания (memorizing) еерезультатов,чтобывдальнейшем,когдавывызоветеэтужефункциюстеми же аргументами, функция смогла воспользоваться тем жерезультатом.

Представьте себе обычный сервер, получающий запросы в видестрок, содержащих кодLua.Каждыйраз приполучении запроса сервервыполняет load для полученной строки и затем вызывает полученнуюфункцию.Однакоload—затратнаяфункция,инекоторыекомандыдлясерверамогутдовольночастоповторяться.Вместоповторноговызоваload каждый раз, когда сервер получает распространенную командувроде "closeconnection()", сервер может запомнить результат load припомощи вспомогательной таблицы. Перед вызовом load серверпроверяет по этой таблице — не была ли уже преобразована даннаястрока.Еслисервернеможетнайтиэтустроку, тогда (и толькотогда)он вызывает load и сохраняет результат в этой таблице. Мы можемзаложитьэтоповедениевновуюфункцию:

localresults={}

functionmem_loadstring(s)

localres=results[s]

ifres==nilthen--результатнедоступен?

res=assert(load(s))--вычисляетновыйрезультат

results[s]=res--сохраняетдляпоследующего

переиспользования

end

returnres

end

Выигрышотэтойсхемыможетбытьогромным.Однако,онатакжеможетпривести кнепредвиденным затратамресурсов.Хотянекоторыекоманды повторяются снова и снова, многие другие командывыполняютсялишьоднажды.Таблицаresultsпостепеннособираетвсекоманды, которые сервер когда-либо получал, вместе с их кодами;спустя некоторое время это может привести к исчерпанию памяти насервере. Слабые таблицы предоставляют простое решение даннойпроблемы. Если таблица results хранит слабые значения, то каждыйцикл сборки мусора удалит все неиспользуемые на данный моментпреобразования(тоестьпрактическивсе):

localresults={}

setmetatable(results,{__mode="v"})--делаетзначенияслабыми

Page 221: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

functionmem_loadstring(s)

<какпрежде>

На самом деле, поскольку индексы всегда являются строками, прижеланиимыможемсделатьэтутаблицуполностьюслабой:

setmetatable(results,{__mode="kv"})

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

уникальности объекта определенного вида. Например, представимсистему,вкоторойцветапредставленытаблицамисполямиred,greenиblue.Примитивнаяфабрикацветов генерируетновыйцветпри каждомновомзапросе:

functioncreateRGB(r,g,b)

return{red=r,green=g,blue=b}

end

Используя технику запоминания, мы можем многократно использоватьодну и ту же таблицу для одного и того же цвета. При созданииуникального ключа для каждого цвета мы просто конкатенируеминдексыцветапосредствомкакого-либоразделителя:

localresults={}

setmetatable(results,{__mode="v"})--делаетзначенияслабыми

functioncreateRGB(r,g,b)

localkey=r.."-"..g.."-"..b

localcolor=results[key]

ifcolor==nilthen

color={red=r,green=g,blue=b}

results[key]=color

end

returncolor

end

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

Page 222: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

будет существоватьдостаточнодолго,чтобыпригодитьсядляновогоцвета.

17.3.Атрибутыобъекта

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

Когдаобъектявляетсятаблицей,мыможемхранитьатрибутвсамойтаблице, подобрав подходящий уникальный ключ.Какмыуже видели,простой и безошибочный способ создать уникальный ключ — этосоздать новый объект (обычно таблицу) и применять его в качествеключа. Однако, если объект не является таблицей, то он не можетхранитьсвоисобственныеатрибуты.Дажевслучаетаблицнамневсегданужно хранить атрибут в исходном объекте. Например, нам можетпонадобится хранить подобный атрибут закрытым или нам не нужно,чтобыатрибутмешалобходутаблицы.Вовсехэтихслучаяхнамнуженинойспособсвязыванияатрибутовсобъектами.

Конечно, внешняя таблица предоставляет идеальный способприсоединенияатрибутовкобъектам(неслучайно,чтотаблицыиногданазывают ассоциативными массивами). Мы используем объекты какключи,аихатрибуты—какзначения.Внешняятаблицаможетхранитьатрибуты объектов любого типа, так как Lua позволяет использоватьобъектылюбоготипавкачествеключейтаблицы.Болеетого,атрибуты,хранящиесявовнешнейтаблице,невлияютнадругиеобъектыимогутбытьзакрытыми,такжекакисаматаблица.

Однако, это на первый взгляд идеальное решение обладаетогромнымнедостатком:кактолькомыиспользовалиобъектвкачествеключа в таблице, мы обрекли его на вечное существование. Lua неможет утилизировать объект, который используется в качестве ключа.Еслимыиспользуемобычнуютаблицу,чтобыпривязатькфункциямихимена, то ни одна из этих функций никогда не будет удалена. Как вывероятнодогадались,мыможемизбежатьэтогонедостаткаприпомощислабых таблиц. Однако, на этот раз нам понадобятся слабые ключи.Применениеслабыхключейнепредотвращаетихутилизацию,когдананихнеостаетсябольшессылок.Сдругойстороны,утаблицынемогутбыть слабые значения; иначе атрибуты существующих объектов могли

Page 223: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

быбытьутилизированы.

17.4.Вновьтаблицысозначениямипоумолчанию

Вразделе13.4мыобсуждали,какреализоватьтаблицысозначениямипоумолчанию,отличнымиотnil.Мырассмотрелиодинчастныйподходизаметили,чтовдвухдругихподходахприменяютсяслабыетаблицы,поэтомуихмыотложилинапотом.Теперьпоравернутьсякэтойтеме.Как вы увидите, эти два решения задачи реализации значений поумолчанию на самом деле являются частными случаями ужерассмотренныхподходов:атрибутовобъектовизапоминания.

В первом решениимыиспользуем слабую таблицу, чтобы связать скаждойтаблицейеезначенияпоумолчанию:

localdefaults={}

setmetatable(defaults,{__mode="k"})

localmt={__index=function(t)returndefaults[t]end}

functionsetDefault(t,d)

defaults[t]=d

setmetatable(t,mt)

end

Если бы у defaults не было слабых ключей, то все эти таблицы созначениямипоумолчаниюсуществовалибыпостоянно.

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

localmetas={}

setmetatable(metas,{__mode="v"})

functionsetDefault(t,d)

localmt=metas[d]

ifmt==nilthen

mt={__index=function()returndend}

metas[d]=mt--memorize

end

setmetatable(t,mt)

end

В данном случае мы применяем слабые значения, чтобы разрешитьутилизациюуженеиспользуемыхметатаблиц.

Какая из этих двух реализаций является лучшей? Как обычно, этозависит от обстоятельств. У обеих схожая сложность и схожее

Page 224: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

быстродействие.Перваяреализациятребуетнесколькихслов(1слово=2байта) памяти для каждой таблицы со значением по умолчанию (длязаписейвdefaults).Втораяреализациятребуетнесколькихдесятковсловпамятидлякаждогоотдельногозначенияпоумолчанию(новаятаблица,новое замыканиеи запись вmetas).Поэтому если в вашемприложениитысячи таблиц с всего несколькими различными значениями поумолчанию, то второе решение явно будет лучше. С другой стороны,еслинесколькотаблицобладаютобщимизначениямипоумолчанию,товамлучшепредпочестьпервуюреализацию.

17.5.Эфемерныетаблицы

Сложная ситуация возникает, когда в таблице со слабыми ключамизначениессылаетсянасвойсобственныйключ.

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

functionfactory(o)

returnfunction()returnoend

end

Этафабрикаявляетсяхорошимкандидатомдлязапоминания,чтобынесоздаватьновоезамыкание,когдаужеестьготовое:

do

localmem={}

setmetatable(mem,{__mode="k"})

functionfactory(o)

localres=mem[o]

ifnotresthen

res=function()returnoend

mem[o]=res

end

returnres

end

end

Однако, здесь есть один подвох. Обратите внимание, что значение(функция-константа), связанное с объектом внутри mem, ссылается насвой собственный ключ (сам объект). Хотя ключи в этой таблицеслабые,значенияслабыминеявляются.Пристандартнойинтерпретациислабых таблиц ничто не будет удалено из таблицы с запоминанием.

Page 225: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Посколькузначениянеявляютсяслабыми,всегдаестьсильнаяссылканакаждуюфункцию.Каждаяфункцияссылаетсянасвойсоответственныйобъект, поэтому на каждый объект всегда есть сильная ссылка. Такимобразом, несмотря на слабые ключи, эти объекты не будутутилизированы.

Однако, эта ограниченная интерпретация не очень удобна.Большинство ожидает, что значение в таблице доступно только черезсоответствующий ключ. Поэтому мы можем рассматриватьвышеописанныйслучайвкачестверазновидностицикла,гдезамыканиессылается на объект, который (через таблицу с запоминанием) в своюочередьссылаетсянаэтозамыкание.

Lua5.2решаетданнуюпроблемуприпомощиконцепцииэфемерныхтаблиц.ВLua5.2таблицасослабымиключамиисильнымизначениямиявляется эфемерной таблицей (ephemeron table). В эфемерной таблицедоступность ключа управляет доступностью соответствующегозначения. В частности, рассмотрим запись (k, v) в эфемерной таблице.Ссылканаvявляетсясильной,толькоеслисуществуетсильнаяссылканаk. В противном случае запись со временем удаляется из таблицы, дажееслиvссылается(прямоиликосвенно)наk.

17.6.Финализаторы

ХотязадачейсборщикамусораявляетсяутилизацияобъектовLua,онтакжеможетпомочьпрограммамсосвобождениемвнешнихресурсов.Сэтой целью некоторые языки программирования предлагают механизмфинализаторов. Финализатор (finalizer) — это функция, связанная собъектом, которая вызывается перед тем, как объект будет удаленсборщикоммусора.

Lua реализует финализаторы при помощи метаметода __gc.Посмотритенаследующийпример:

o={x="hi"}

setmetatable(o,{__gc=function(o)print(o.x)end})

o=nil

collectgarbage()-->hi

В этом примере мы сперва создаем таблицу и устанавливаем для нееметатаблицу, у которой есть метаметод __gc. Затем мы уничтожаемединственную ссылку на эту таблицу (глобальная переменная о) изапускаемполнуюсборкумусораприпомощивызоваcollectgarbage.Во

Page 226: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

времясборкимусораLuaобнаруживает,чтоданнаятаблицанеявляетсядоступнойивызываетеефинализатор(метаметод__gc).

У финализаторов Lua есть один нюанс, связанный с пометкойобъектадляфинализации.Мыпомечаемобъектдляфинализации,когдазадаемдлянегометатаблицусненулевымметаметодом__gc.Еслимынепометим объект, то он не будет финализирован. Большая часть кода,которыймыпишем,работаетожидаемымобразом,ноиногдавозникаютстранныеслучаивродеследующего:

o={x="hi"}

mt={}

setmetatable(o,mt)

mt.__gc=function(o)print(o.x)end

o=nil

collectgarbage()-->(ничегонепечатает)

В этом примере метатаблица, которую мы устанавливаем для о, несодержит метаметода __gc, поэтому объект и не помечается дляфинализации.Дажееслипотомдобавитьполе__gcметатаблице,Luaнепосчитает это присваивание каким-то особенным, так что объект небудет помечен. Как мы и сказали, это редко бывает проблемой;метаметоды редко изменяются после начала использованияметатаблицы.

Если вы действительно хотите задать метаметод позже, то выможете использовать любое значение для поля __gc в качествевременного:

o={x="hi"}

mt={__gc=true}

setmetatable(o,mt)

mt.__gc=function(o)print(o.x)end

o=nil

collectgarbage()-->hi

Теперь, поскольку метатаблица содержит поле __gc, объект о

надлежащим образом помечается для финализации. Нет никакойпроблемы в том, чтобы задать метаметод позже; Lua вызываетфинализатор,толькоеслионявляетсясоответственнойфункцией.

Когда сборщик мусора утилизирует несколько объектов в одном итомжецикле,онвызываетихфинализаторывпорядке,обратномтому,вкотором объекты были помечены для финализации. Рассмотримследующий пример, который создает связанный список объектов сфинализаторами:

Page 227: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

mt={__gc=function(o)print(o[1])end}

list=nil

fori=1,3do

list=setmetatable({i,link=list},mt)

end

list=nil

collectgarbage()

-->3

-->2

-->1

Первым финализируемым объектом будет объект 3, который былпоследнимпомеченнымобъектом.

Считать, что ссылки между утилизируемыми объектами могутповлиятьнапорядокихфинализации—распространенноезаблуждение.Например, можно подумать, что объект 2 в предыдущем примередолжен быть финализирован перед объектом 1, поскольку существуетссылкаот2к1.Однако,ссылкимогутформироватьциклы.Поэтомуониникакневлияютнапорядокфинализации.

Другим неочевидным моментом, связанным с финализаторами,является восстановление (resurrection). При своем вызове финализаторполучаетвкачествепараметрафинализируемыйобъект.Такимобразом,объектсновастановитсяживым,покрайнеймеренавремяфинализации.Я называю это временным восстановлением (transient resurrection). Вовремявыполненияфинализатораничтонемешаетемусохранитьобъект,скажем, в глобальной переменной, чтобы объект остался доступнымпосле возврата из финализатора. Я называю это перманентнымвосстановлением(permanentresurrection).

Восстановление должно быть временным. Рассмотрим следующийфрагменткода:

A={x="thisisA"}

B={f=A}

setmetatable(B,{__gc=function(o)print(o.f.x)end})

A,B=nil

collectgarbage()-->thisisA

ФинализатордляBобращаетсякA,поэтомуAнеможетбытьудалендофинализации B. Lua должен восстановить и A, и B перед вызовомфинализатора.

Из-за восстановления объекты с финализаторами собираются в дваэтапа. Вначале, когда сборщик мусора обнаруживает, что объект сфинализатором недостижим, он восстанавливает этот объект идобавляетегокочередифинализации.Послевыполненияфинализатора

Page 228: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Lua помечает объект как финализированный. В следующий раз, когдасборщикмусораобнаружит,чтообъектнедостижим,онегоуничтожит.Если вы хотите быть уверенными в том, что весь мусор в вашейпрограмме был действительно собран, то вы должны вызватьcollectgarbage дважды; второй вызов уничтожит объекты, которыебылифинализированывовремяпервоговызова.

Финализатор для каждого объекта выполняется ровно один раз,поскольку Lua ставит пометку на финализированные объекты. Еслиобъектне был собран сборщикомдо концаработыпрограммы, тоLuaвызовет его финализатор при закрытии всего состояния Lua. Этапоследняя особенность позволяет реализовать в Lua аналог функцийatexit, то есть функций, которые вызываются непосредственно передзавершениемпрограммы.Все,чтодляэтогонужно,—создатьтаблицусфинализатором и закрепить ее где-нибудь, например в глобальнойпеременной:

_G.AA={__gc=function()

--поместитездесьваш'atexit'-код

print("finishingLuaprogram")

end}

setmetatable(_G.AA,_G.AA)

Другойинтересныйподходпозволяет вызывать заданнуюфункциюкаждый раз, когда Lua завершает цикл сборки мусора. Посколькуфинализатор выполняется ровно один раз, то хитрость здесь в том,чтобы финализатор создавал новый объект для вызова следующегофинализатора:

do

localmt={__gc=function(o)

--делайтевсе,чтохотите

print("newcycle")

--создаетновыйобъектдляследующегоцикла

setmetatable({},getmetatable(o))

end}

--создаетпервыйобъект

setmetatable({},mt)

end

collectgarbage()-->новыйцикл

collectgarbage()-->новыйцикл

collectgarbage()-->новыйцикл

Вовзаимодействииобъектовсфинализаторамиислабымитаблицами

Page 229: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

тоже естьнюанс.Сборщикмусораочищает значения в слабой таблицеперед восстановлением, в то время как ключи очищаются послевосстановления. Следующий фрагмент кода иллюстрирует этоповедение:

--таблицасослабымиключами

wk=setmetatable({},{__mode="k"})

--таблицасослабымизначениями

wv=setmetatable({},{__mode="v"})

o={}--объект

wv[1]=o;wk[o]=10--добавляетихкобеимтаблицам

setmetatable(o,{__gc=function(o)

print(wk[o],wv[1])

end})

o=nil;collectgarbage()-->10nil

Вовремявыполненияфинализатораоннаходитобъектвтаблицеwk,ноне в таблице wv. Обоснованием такого поведения является то, что мычастохранимсвойстваобъектавтаблицахсослабымиключами(какмыобсудиливразделе17.3),ифинализаторамможетпонадобитьсядоступк этим атрибутам. Однако, мы используем таблицы со слабымизначениямидлямногократного использованияживых объектов; в этомслучаефинализируемыеобъектыстановятсябесполезны.

Упражнения

Упражнение 17.1. Напишите проверочный код, чтобы определить,действительно ли Lua использует эфемерные таблицы. (Не забудьтевызвать collectgarbage для принудительного цикла сборки мусора.) Повозможности проверьте ваш код как в Lua 5.1, так и в Lua 5.2, чтобыувидетьразницу.

Упражнение 17.2. Рассмотрим первый пример из раздела 17.6,который создает таблицу с финализатором, печатающим сообщениелишьпривызове.Чтопроизойдет,еслипрограммазавершитсябезцикласборки мусора? Что случится, если программа вызовет os.exit? Чтопроизойдет,еслипрограммазавершитсвоевыполнениесошибкой?

Упражнение 17.3. Допустим, вам нужно реализовать таблицу сзапоминанием для функции, получающей строку и возвращающейстроку. Применение слабой таблицы не позволит удалять записи,

Page 230: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

поскольку слабые таблицы не рассматривают строки как удаляемыеобъекты.Каквыможетереализоватьзапоминаниевэтомслучае?

Упражнение17.4.Объяснитевыводследующейпрограммы:localcount=0

localmt={__gc=function()count=count-1end}

locala={}

fori=1,10000do

count=count+1

a[i]=setmetatable({},mt)

end

collectgarbage()

print(collectgarbage"count"*1024,count)

a=nil

collectgarbage()

print(collectgarbage"count"*1024,count)

collectgarbage()

print(collectgarbage"count"*1024,count)

Page 231: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ЧастьIII

Стандартныебиблиотеки

Page 232: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА18

Математическаябиблиотека

В этой и следующих главах, посвященных стандартнымбиблиотекам, моей целью является не дать полную спецификациюкаждой функции, а показать, какую функциональность предоставляеткаждаябиблиотека.Дляясностиизложенияямогуопускатьнекоторыеспецифические опции или режимы работы. Главной целью являетсязажечь в вас любопытство, которое затем может быть удовлетвореночтениемсправочникаLua.

Библиотека math содержит стандартный набор математическихфункций,такихкактригонометрическиефункции(sin,cos,tan,asin,acosи т. п.), возведение в степень и логарифмирование (exp, log, log10),функции округления (floor, ceil), min, max, функции для генерациипсевдослучайныхчисел(random,randomseed),переменнаяpiипеременнаяhuge, которая является наибольшим представимым в Lua числом (нанекоторыхплатформахможетприниматьспециальноезначениеinf).

Всетригонометрическиефункцииработаютсрадианами.Выможетеиспользовать функции deg и rad для перевода между градусами ирадианами. Если вы хотите работать с градусами, вы можетепереопределитьтригонометрическиефункции:

do

localsin,asin,...=math.sin,math.asin,...

localdeg,rad=math.deg,math.rad

math.sin=function(x)returnsin(rad(x))end

math.asin=function(x)returndeg(asin(x))end

...

end

Функцияmath.random генерируетпсевдослучайныечисла.Выможетевызыватьеетремяспособами.Когдамывызываемеебезаргументов,онавозвращает вещественное псевдослучайное число с равномернымраспределением в диапазоне [0,1). Когда мы вызываем ее сединственным аргументом, целочисленным n, то она возвращаетпсевдослучайное целое число х, такое, что 1 ≤ x ≤ n. Например, выможете смоделировать бросок кубика при помощи random(6). Наконец,мыможем вызвать random с двумя целочисленными аргументами l и u,

Page 233: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

чтобыполучитьпсевдослучайноецелоечислоx,такоечтоl≤x≤u.Вы можете задать затравку (seed) для генератора псевдослучайных

чисел при помощи функции randomseed; затравка является ееединственным числовым аргументом.Обычно, при запуске программыона инициализирует генератор некоторым фиксированным значением.Это значит, что каждый раз, когда вы запускаете вашу программу, онагенерирует одну и ту же последовательность псевдослучайных чисел.Для отладки это качество весьма удобно, но в игре все будетпроисходить по одному и тому же сценарию снова и снова.Распространенным приемом для решения данной проблемы являетсяиспользование в качестве затравки текущего времени путем вызоваmath.randomseed(os.time()). Функция os.time возвращает число,представляющее текущее время, обычно в виде числа секунд,прошедшихсопределенногомоментавремени.

Функция math.random использует функцию rand из стандартнойбиблиотекиС.Внекоторыхреализацияхэтафункциявозвращаетчисласне очень хорошими статистическими свойствами. Вы можетепопробоватьотыскатьболееудачныйгенераторпсевдослучайныхчиселвнезависимыхдистрибутивах.(СтандартнаяпоставкаLuaневключаетвсебяподобного генератора воизбежаниепроблем с авторскимправом.Онасодержиттолькокод,написанныйавторамиLua.)

Упражнения

Упражнение18.1.Напишитефункциюдляпроверкитого,являетсялизаданноечислостепеньюдвойки.

Упражнение 18.2. Напишите функцию для расчета объема прямогокруговогоконусапоеговысотеиуглумеждуегообразующейиосью.

Упражнение 18.3. Реализуйте другой генератор псевдослучайныхчисел в Lua. Поищите хороший алгоритм в Интернете. (Вам можетпонадобитьсяпобитоваябиблиотека;см.главу19.)

Упражнение 18.4. Используя math.random, напишите функцию дляполучения псевдослучайных чисел со стандартным нормальнымраспределением(поГауссу).

Упражнение18.5.Напишитефункциюдляперемешиваниязаданногосписка.Убедитесь,чтовсевариантыравновероятны.

Page 234: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА19

Побитоваябиблиотека

ИсточникомпостоянныхжалобнасчетLuaявляетсяотсутствиевнемпобитовых операций. Это отсутствие вовсе не случайно. Не так легкопомиритьпобитовыеоперациисчисламисплавающейточкой.

Мы можем выразить некоторые побитовые операции какарифметические операции. Например, сдвиги влево соответствуютумножению на степени двух, сдвиги направо соответствуют делению,Однако, у побитовых AND и OR нет таких арифметических аналогов.Они определены для двоичных представлений целых чисел.Практически невозможно расширить их на операции с плавающейточкой.Даженекоторыепростыеоперациитеряютсмысл.Чтодолжнобытьдополнением0.0?Должнолиэтобытьравно-1?Или0xFFFFFFFF(чтовLuaравно4294967295,чтоявнонеравно-1)?Илиможетбыть264-1 (число, которое нельзя точно представить при помощи значениятипаdouble)?

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

Для большинства примеров в этой главе я буду использоватьшестнадцатеричную запись. Я буду использовать слово мах дляобозначения0xFFFFFFFF(тоесть232-1).Впримерахябудуиспользоватьследующуюдополнительнуюфункцию:

functionprintx(x)

print(string.format("0x%X",x))

end

Побитовая библиотека в Lua 5.2 называется bit32. Как следует изимени, она работает с 32-битовыми числами.Посколькуand,or и notявляются зарезервированными в Lua словами, то соответствующиефункцииназваныband,bor иbnot.Для последовательности в названиях

Page 235: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

функцияпобитовогоисключающегоИЛИназванаbхог:printx(bit32.band(0xDF,0xFD))-->0xDD

printx(bit32.bor(0xD0,0x0D))-->0xDD

printx(bit32.bxor(0xD0,0xFF))-->0x2F

printx(bit32.bnot(0))-->0xFFFFFFFF

Функцииband,borиbxorпринимаютлюбоеколичествоаргументов:printx(bit32.bor(0xA,0xA0,0xA00))-->0xAAA

printx(bit32.band(0xFFA,0xFAF,0xAFF))-->0xAAA

printx(bit32.bxor(0,0xAAA,0))-->0xAAA

printx(bit32.bor())-->0x0

printx(bit32.band())-->0xFFFFFFFF

printx(bit32.bxor())-->0x0

(Онивсекоммутативныиассоциативны.)Побитоваябиблиотекаработает сбеззнаковымицелымичислами.В

ходе работы любое число, переданное как аргумент, приводится кцелому числу в диапазоне 0—MAX. Во-первых, неуказанные числаокругляютсянеуказаннымспособом.Во-вторых,числавнедиапазона0—МАХ приводятся к нему при помощи операции остатка от деления:целое n становится n%232. Эта операция эквивалентна получениюдвоичногопредставлениячислаизатемвзятиюегомладших32бит.Каки ожидается, -1 становится MAX. Вы можете использовать следующиеоперациидлянормализациичисла(тоестьотображенияеговдиапазон0-MAX):

printx(bit32.bor(2^32))-->0x0

printx(bit32.band(-1))-->0xFFFFFFFF

Конечно,встандартномLuaлегчепростовыполнитьn%(2^32).Если явно не указано, все функции в библиотеке возвращают

результат, который также лежит в 0—MAX. Однако, вам следует бытьосторожными при использовании результатов побитовых операций вкачествеобычныхчисел.ИногдаLuaкомпилируется,используядругойтип для чисел. В частности, некоторые системы с ограниченнымивозможностямииспользуют32-битовыечиславкачествечиселвLua.Вэтих системах MAX равен -1. Более того, некоторые побитовыебиблиотеки используют различные соглашения для своих результатов.Поэтомувсякийраз,когдавамнужноиспользоватьрезультатпобитовойоперации в качестве числа, будьте осторожны. Избегайте сравнений:вместо х<0 напишите bit32.btest(х,0x80000000). (Мы скоро увидимфункцию btest.) Используйте саму побитовую библиотеку для

Page 236: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

нормализацииконстант:ifbit32.or(a,b)==bit32.or(-1)then

<какой-нибудькод>

Побитовая библиотека также определяет операции для сдвига ивращения бит: lshift для сдвига налево; rshift и arshift

(арифметический сдвиг) для сдвига направо; lrotate для вращенияналево и rrotate для вращения направо. За исключениемарифметического сдвига (arshift), все сдвиги заполняют новые битынулями. Арифметический сдвиг заполняет биты слева копиями своегопоследнегобита(«сигнальнымбитом»):

printx(bit32.rshift(0xDF,4))-->0xD

printx(bit32.lshift(0xDF,4))-->0xDF0

printx(bit32.rshift(-1,28))-->0xF

printx(bit32.arshift(-1,28))-->0xFFFFFFFF

printx(bit32.lrotate(0xABCDEF01,4))-->0xBCDEF01A

printx(bit32.rrotate(0xABCDEF01,4))-->0x1ABCDEF0

Сдвигиливращениенаотрицательноечислобитсдвигает(вращает)в противоположную сторону. Например, сдвиг на -1 бит направоэквивалентен сдвигу на 1 бит влево. Результат сдвига на более чем 31битравен0илиMAX,посколькувсеисходныебитыпропали:

printx(bit32.lrotate(0xABCDEF01,-4))-->0x1ABCDEF0

printx(bit32.lrotate(0xABCDEF01,-36))-->0x1ABCDEF0

printx(bit32.lshift(0xABCDEF01,-36))-->0x0

printx(bit32.rshift(-1,34))-->0x0

printx(bit32.arshift(-1,34))-->0xFFFFFFFF

Кроме этих, более или менее стандартных операций, побитоваябиблиотека также предоставляет три дополнительные функции.Функцияbtestосуществляеттужеоперацию,чтоиband,новозвращаетрезультатсравненияпобитовойоперацииснулем:

print(bit32.btest(12,1))-->false

print(bit32.btest(13,1))-->true

Другой распространенной операцией является извлечение заданныхбитов из числа. Обычно эта операция включает в себя сдвиг ипобитовое AND; побитовая библиотека упаковывает все это в однуфункцию.Вызовbit32.extract(х,f,w)возвращаетw битизх, начиная сбитаf:

printx(bit32.extract(0xABCDEF01,4,8))-->0xF0

Page 237: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

printx(bit32.extract(0xABCDEF01,20,12))-->0xABC

printx(bit32.extract(0xABCDEF01,0,12))-->0xF01

Эта операция считает биты от 0 до 31. Если третий аргумент (w) незадан,тоонсчитаетсяравнымединице:

printx(bit32.extract(0x0000000F,0))-->0x1

printx(bit32.extract(0xF0000000,31))-->0x1

Обратной к операции extract является операция replace, котораязаменяет заданныебиты.Первымпараметромявляетсяисходноечисло.Второйпараметрзадаетзначение,котороенадовставить.Последниедвапараметра,fиw,имеюттотжесмысл,чтоивbit32.extract:

printx(bit32.replace(0xABCDEF01,0x55,4,8))-->0xABCDE551

printx(bit32.replace(0xABCDEF01,0x0,4,8))-->0xABCDE001

Обратите внимание, что для любых допустимых значений х, f и wвыполняетсяследующееравенство:

assert(bit32.replace(x,bit32.extract(x,f,w),f,w)==x)

Упражнения

Упражнение 19.1. Напишите функцию для проверки того, чтозаданноечислоявляетсястепеньюдвух.

Упражнение 19.2. Напишите функцию для вычисления весаХэмминга заданного целого числа. (Вес Хэмминга для числа — этоколичествоединичныхбитвдвоичномпредставленииэтогочисла).

Упражнение 19.3. Напишите функцию для проверки того, являетсялидвоичноепредставлениечислапалиндромом(перевертышем).

Упражнение 19.4. Определите операции сдвига и побитовый ANDприпомощиарифметическихоперацийLua.

Упражнение 19.5. Напишите функцию, которая получает строку,закодированную в UTF-8, и возвращает ее первый символ как число.Функциядолжнавернутьnil,еслистроканеначинаетсясдопустимойвUTF-8последовательности.

Page 238: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА20

Табличнаябиблиотека

Библиотекаtableсостоитизвспомогательныхфункцийдляработыстаблицамикаксмассивами.Онапредоставляетфункциидлявставкииудаленияэлементовизсписков,длясортировкиэлементовмассиваидляконкатенациивсехстроквмассиве.

20.1.Функцииinsertиremove

Функция table.insert вставляет элемент в заданную позициюмассива, отодвигая остальные элементы, чтобы освободить место.Например, если t — это массив {10, 20, 30), то после вызоваtable.insert(t,1,15)массивtстанетравен{15,10,20,30}.Вкачествеособого (и частого) случая, если мы вызовем insert без указанияпозиции, то она вставит элемент в последнюю позицию массива (ипотому не станет сдвигать элементы). В качестве примера следующийкодпострочночитаетвводпрограммы,хранявсестрокивмассиве:

t={}

forlineinio.lines()do

table.insert(t,line)

end

print(#t)-->(количествосчитанныхстрок)

ВLua5.0даннаяидиомабылаширокораспространена.Вболеепозднихверсиях я предпочитаю идиому t[#t + 1] = line, чтобы добавитьэлементыксписку.

Функция table.remove удаляет (и возвращает) элемент с заданнойпозициимассива, при этом сдвигая остальные элементы, чтобы занятьего место. При вызове без указания позиции она удаляет последнийэлементмассива.

Припомощи этихдвухфункцийдовольнолегко реализовать стеки,очереди и двойные очереди. Мы можем инициализировать подобныеструктуры как t={}. Операция заталкивания эквивалентнаtable.insert(t,х);операциявыталкиванияэквивалентнаtable.remove(t).Вызов table.insert(t,1,x) вставляет элемент в другой конец этойструктуры (по сути, в ее начало), а вызов table.remove(t,1) удаляет

Page 239: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

элемент с этого конца. Две последние операции не особенноэффективны, поскольку они должны сдвигать и отодвигать элементы.Однако,учитывая,чтобиблиотекаtableреализовананаС,этициклынеслишком затратны и потому данная реализация довольно хороша длянебольшихмассивов(скажем,донесколькихсотэлементов).

20.2.Сортировка

Ещеоднойполезнойфункциейдлямассивовявляетсяtable.sort;мыуже видели ее прежде. Она принимает массив и необязательнуюфункцию упорядочения. Эта функция принимает два аргумента идолжна вернуть true, когда ее первый аргумент должен идти передвторым в отсортированном массиве. Если эта функция непредоставлена,sortпоумолчаниюиспользуетсравнение«меньше,чем»(соответствующееоперации'<').

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

lines={

luaH_set=10,

luaH_get=24,

luaH_present=48,

}

А теперь вам нужно напечатать имена этих функций в алфавитномпорядке. Если вы обойдете эту таблицу при помощи pairs, то именаокажутся в произвольном порядке. Вы не можете отсортировать ихнепосредственно, поскольку эти имена являются ключами таблицы.Однако,еслипоместитьихвмассив,товысможетеихотсортировать.Сначала вы должны создать массив с этими именами, затемотсортироватьегоивитогенапечататьрезультат:

a={}

forninpairs(lines)doa[#a+1]=nend

table.sort(a)

for_,ninipairs(a)doprint(n)end

Page 240: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Некоторых это смущает. В конце концов, в массивах Lua тоже нетникакогопорядка(онижевсе-такитаблицы).Норазмыумеемсчитать,мы навяжем порядок при обращении к массиву с помощьюупорядоченныхиндексов.Именнопоэтомувывсегдадолжныобходитьмассивыприпомощиipairs,анеpairs.Перваяфункциязадаетпорядокключей 1, 2, ..., в то время как вторая работает с естественнымпроизвольнымпорядкомтаблицы.

В качестве более продвинутого решения мы можем написатьитератор, который перебирает таблицу, следуя порядку ее ключей.Необязательный параметр f позволяет задать другой порядок. Этотитератор сначала сортирует ключи в отдельный массив, а затем ужеобходит этот массив. На каждом шаге он возвращает ключ и егозначениеизисходноготаблицы:

functionpairsByKeys(t,f)

locala={}

forninpairs(t)doa[#a+1]=nend

table.sort(a,f)

locali=0--итерационнаяпеременная

returnfunction()--итерирующаяфункция

i=i+1

returna[i],t[a[i]]

end

end

Припомощиэтойфункцииможнолегконапечататьименатехфункцийвалфавитномпорядке:

forname,lineinpairsByKeys(lines)do

print(name,line)

end

20.3.Конкатенация

Мыужевиделиtable.concatвразделе11.6.Онаберетсписокстрокивозвращает результат конкатенации всех этих строк. Необязательныйвторойаргументзадаетразделительстрок,которыйвставляетсямеждустроками из списка. Данная функция также принимает два другихнеобязательныхаргумента,которыезадаютиндексыпервойипоследнейконкатенируемыхстрок.

Следующаяфункцияявляетсяинтереснымобобщениемtable.concat.Онаспособнаприниматьвложенныеспискистрок:

functionrconcat(l)

Page 241: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

iftype(l)~="table"thenreturnlend

localres={}

fori=1,#ldo

res[i]=rconcat(l[i])

end

returntable.concat(res)

end

Длякаждогоэлементаспискафункцияrconcatрекурсивновызываетсебядля конкатенации возможного вложенного списка. Затем она вызываетисходную table.concat для объединения всех промежуточныхрезультатов.

print(rconcat{{"a",{"nice"}},"and",{{"long"},{"list"}}})

-->аккуратныйидлинныйсписок

Упражнения

Упражнение20.1.Перепишитефункциюrconcatтак,чтобыонамоглаприниматьразделитель,какэтоделаетtable.concat:

print(rconcat({{{"a","b"},{"c"}},"d",{},{"e"}},";")

-->a;b;c;d;e

Упражнение20.2.Проблемасtable.sortвтом,чтоэтасортировканеустойчива, то есть элементы, которые функция упорядочения считаетравными,могутинесохранитьсвойпервоначальныйпорядоквмассивепосле сортировки. Как можно реализовать устойчивую сортировку вLua?

Упражнение 20.3. Напишите функцию для проверки того, являетсялизаданнаятаблицадопустимойпоследовательностью.

Page 242: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА21

Строковаябиблиотека

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

Строковая библиотека экспортирует своифункции в видемодуля сименемstring.НачинаясLua5.1,онатакжеэкспортируетсвоифункциикак методы, принадлежащие типу string (при помощи метатаблицыданного типа). Поэтому, например, перевод строки в заглавные буквыможнозаписатьлибокакstring.upper(s),либокакs:upper().Выбирайтесами.

21.1.Основныестроковыефункции

Некоторые функции в строковой библиотеке крайне просты: вызовstring.len(s) возвращает длину строки s. Это эквивалентно #s. Вызовstring.rep(s,n) (илиs:rep(n)) возвращает строку s, повторенную n раз.Выможете создать строку в 1Мб (например, для тестов)припомощиstring.rep("а",2^20). Вызов string.lower(s) возвращает копию s, укоторой заглавные буквы преобразованы в строчные; все прочиесимволы остаются прежними. (Функция string.upper преобразуетстрочные буквы в заглавные.) Обычно, когда требуется отсортироватьстроки независимо от регистра их букв, можно написать что-то вродеэтого:

table.sort(a,function(a,b)

returna:lower()<b:lower()

end)

Вызов string.sub(s,i,j) извлекает часть строки s от символа синдексомiпосимволсиндексомjвключительно.ВLuaиндекспервогосимволастрокиравен1.Приэтомвыможетеприменятьотрицательныеиндексы, которые отсчитываются с конца строки: индекс -1 ссылаетсянапоследнийсимволстроки,-2напредпоследнийсимволит.д.Таким

Page 243: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

образом, вызов string.sub(s,1,j) (или s:sub(1,j)) возвращает префиксстроки s c длиной, равной j; вызов string.sub(s,j,-1) (или простоs:sub(j),посколькузначениемпоумолчаниюдляпоследнегоаргументаявляется -1) возвращает окончание строки, начиная с символа синдексом j; а вызов string.sub(s,2,-2) возвращает копию строки s, вкоторойудаленыпервыйипоследнийсимволы:

s="[inbrackets]"

print(s:sub(2,-2))-->inbrackets

Помните, что строки вLuaнеизменяемы.Функцияstring.sub, как илюбаядругаяфункциявLua,неизменяетзначениестроки,авозвращаетновую строку. Написать нечто вроде s:sub(2,-2) и ждать, что этоизменитзначениестрокиs,—распространеннаяошибка.Есливыхотитеизменить значение переменной, то вы должны присвоить ей новоезначение:

s=s:sub(2,-2)

Функцииstring.charиstring.byteвыполняютпреобразованиемеждусимволами и их внутренними числовыми представлениями. Функцияstring.charпринимаетнольилиболеецелыхчисел,преобразуеткаждоеиз них в символ и возвращает строку, в которой все эти символысоединены. Вызов string.byte(s,i) возвращает внутреннее числовоепредставление символа с индексом i в строке s; второй аргументнеобязателен, поэтому вызов string.byte(s) возвращает внутреннеечисловоепредставлениепервого(илиединственного)символастрокиs.В следующих примерах мы считаем, что символы представленыкодировкойASCII:

print(string.char(97))-->a

i=99;print(string.char(i,i+1,i+2))-->cde

print(string.byte("abc"))-->97

print(string.byte("abc",2))-->98

print(string.byte("abc",-1))-->99

В последней строке кода мы использовали отрицательный индекс дляобращениякпоследнемусимволустроки.

Начиная с Lua 5.1, функция string.byte поддерживает третийнеобязательный аргумент. Вызов наподобие string.byte(s,i,j) рядзначений в форме числовых представлений всех символов междуиндексамиiиjвключительно:

Page 244: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(string.byte("abc",1,2))

Значениемпоумолчаниюдляjявляетсяi, поэтомувызовбез третьегоаргумента возвращает лишь символ по индексу i. Есть прекраснаяидиома{s:byte(1,-1)},котораясоздаеттаблицускодамивсехсимволовстрокиs.Поэтойтаблицемыможемвоссоздатьисходнуюстрокупутемвызоваstring.char(table.unpack(t)). Этот прием не работает для оченьдлинныхстрок(более1Мб),посколькувLuaестьограничениеначисловозвращаемыхфункциейзначений.

Функция string.format — это мощный инструмент дляформатирования строк, обычно для вывода. Она возвращаетотформатированную версию всех своих аргументов (так как являетсяваридической),следуяописанию,заданномусвоимпервымаргументом,так называемой форматирующей строкой (format string).Форматирующая строка следует правилам, похожим на те, чтоиспользуются в функции printf стандартного С: она состоит изобычного текста и указаний, которые руководят тем, где и как будетпомещен каждый аргумент в отформатированной строке. Указание(directive) состоит из символа '%' и буквы, которая поясняет, какотформатировать аргумент: 'd' для десятичных чисел, 'х' дляшестнадцатеричных чисел, 'о' для восьмеричных, 'f' для чисел сплавающей точкой, 's' для строк; есть и другие варианты.Между '%' ибуквой могут быть включены другие опции, управляющие деталямиформатирования,например, количествомдесятичныхцифрдлячисла сплавающейточкой:

print(string.format("pi=%.4f",math.pi))-->pi=3.1416

d=5;m=11;y=1990

print(string.format("%02d/%02d/%04d",d,m,y))-->05/11/1990

tag,title="h1","atitle"

print(string.format("<%s>%s</%s>",tag,title,tag))

--><h1>atitle</h1>

Впервомпримере%.4fозначаетчислосплавающейточкойсчетырьмяцифрами после десятичной точки. Во втором примере %02d обозначаетдесятичное число минимум из двух цифр с дополнением нулями принеобходимости;указание%2dбезнулядополнялобычислопробелами.ЗаполнымописаниемэтихуказанийобратитеськсправочникупоLuaили,еще лучше, обратитесь к справочнику по С, так как Lua вызываетфункцииизстандартнойбиблиотекиСдлявыполненияданнойтяжелойработы.

Page 245: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

21.2.Функциисопоставлениясобразцом

Наиболее эффективными функциями в строковой библиотекеявляютсяфункци find (поиск), match (сопоставление), gsub (глобальнаязамена) и gmatch (глобальное сопоставление). Все они основаны наобразцах(pattern).

Вотличиеотрядадругихскриптовыхязыков,Luaнеиспользуетдлясопоставления с образцом (pattern matching) регулярные выражения изPOSIX или Perl. Главной причиной этого решения является размер:типичнаяреализациярегулярныхвыраженийPOSIXзанимаетболее4000строк кода. Это больше размера всех вместе взятых стандартныхбиблиотек Lua. Для сравнения реализация сопоставления с образцом вLuaзанимаетменее600строк.Конечно,сопоставлениесобразцомвLuaуступаетполноценнойреализацииPOSIX.Темнеменее,сопоставлениесобразцом в Lua является мощным инструментом и включает в себянекоторые возможности, для которых нелегко подобрать аналог встандартныхреализацияхPOSIX.

Функцияstring.find

Функцияstring.findищетобразецвнутризаданнойобрабатываемойстроки. Простейшей формой образца является слово, котороесоответствуетлишькопиисамогосебя.Например,образец'hello' задастпоиск подстроки "hello" внутри обрабатываемой строки. Принахождении образца find возвращает два значения: индекс, с которогоначинаетсясовпадение,ииндекс,накоторомсовпадениезаканчивается.Еслисовпадениененайдено,возвращаетсяnil:

s="helloworld"

i,j=string.find(s,"hello")

print(i,j)-->15

print(string.sub(s,i,j))-->hello

print(string.find(s,"world"))-->711

i,j=string.find(s,"l")

print(i,j)-->33

print(string.find(s,"lll"))-->nil

Когдасовпадениенайдено,мыможемвызватьstring.subсозначениями,возвращенными string.find, чтобы получить часть обрабатываемойстроки, удовлетворяющей образцу.Для простых образцов этой частьюбудетсамобразец.

Page 246: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Уфункцииstring.findестьнеобязательныйтретийпараметр:индекс,указывающий, с какого места обрабатываемой строки следует начатьпоиск.Этотпараметрудобен,когдамыхотимобработатьвсеиндексы,гдебылобнаружен заданныйобразец:мысистематическиищемновыйобразец,каждыйразначинаястойпозиции,гдемынашлипредыдущий.В качестве примера следующий код строит таблицу с позициями всехпереводовстрокивнутризаданнойстроки:

localt={}--таблицадляхраненияиндексов

locali=0

whiletruedo

i=string.find(s,"\n",i+1)--ищетследующийпереводстроки

ifi==nilthenbreakend

t[#t+1]=i

end

Позжемыувидимболеепростойспособ записиподобныхциклов—сприменениемитератораstring.gmatch.

Функцияstring.match

Функция string.match похожа на string.find в том смысле, что онатожеищетобразецвстроке.Однако,вместовозвращениятехпозиций,гдебылнайденобразец,онавозвращаетчастьобрабатываемойстроки,удовлетворяющуюобразцу:

print(string.match("helloworld","hello"))-->hello

Дляфиксированныхобразцоввроде'hello'этафункциянеимеетсмысла.Она показывает свою эффективность, когда используется спеременнымиобразцами,каквследующемпримере:

date="Todayis17/7/1990"

d=string.match(date,"%d+/%d+/%d+")

print(d)-->17/7/1990

Вскоре мы обсудим значение образца '%d+/%d+/%d+' и болеепродвинутоеприменениеstring.match.

Функцияstring.gsub

Уфункцииstring.gsubтриобязательныхпараметра:обрабатываемаястрока,образецизамещающаястрока.Ееосновноеприменениесостоит

Page 247: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

в замене всех вхождений образца на замещающую строку внутриобрабатываемой:

s=string.gsub("Luaiscute","cute","great")

print(s)-->Luaisgreat

s=string.gsub("alllii","l","x")

print(s)-->axxxii

s=string.gsub("Luaisgreat","Sol","Sun")

print(s)-->Luaisgreat

Необязательныйчетвертыйпараметрограничиваетчисловыполняемыхзамен:

s=string.gsub("alllii","l","x",1)

print(s)-->axllii

s=string.gsub("alllii","l","x",2)

print(s)-->axxlii

Функцияstring.gsub также возвращает в качестве второго значениячисловыполненныхзамен.Например,простойспособпосчитатьчислопробеловвстроке:

count=select(2,string.gsub(str,"",""))

Функцияstring.gmatch

Функцияstring.gmatchвозвращаетфункцию,котораяперебираетвсевхожденияобразцавстроку.Например,следующийпримерсобираетвсесловавзаданнойстрокеs:

words={}

forwinstring.gmatch(s,"%a+")do

words[#words+1]=w

end

Как мы вскоре обсудим, образец '%а+' соответствуетпоследовательностям из одной или более букв (то есть словам).Поэтому цикл for обойдет все слова внутри обрабатываемой строки,сохраняяихвсписокwords.

Следующий пример реализует функцию, аналогичнуюpackage.searchpath,припомощиgmatchиgsub:

functionsearch(modname,path)

modname=string.gsub(modname,"%.","/")

forcinstring.gmatch(path,"[^;]+")do

localfname=string.gsub(c,"?",modname)

Page 248: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localf=io.open(fname)

iffthen

f:close()

returnfname

end

end

returnnil--ненайден

end

Первым шагом будет замена всех точек на разделитель путейдиректорий, которым в данном примере является '/' (Как мы вдальнейшем увидим, у точки в образцах особое назначение. Длясопоставления с символом точки мы должны написать '%.'.) Далеефункцияперебираетвсесоставляющиепути, гдекаждаясоставляющяя—этонепрерывныйрядлюбыхсимволов,отличныхотточкисзапятой.Длякаждойсоставляющейфункцияменяетвсевопросительныезнакинаимямодуляипроверяет,существуетлитакойфайл.Еслида,тофункциязакрываетэтотфайливозвращаетегоимя.

21.3.Образцы

Выможете сделать образцы более полезными при помощи классовсимволов. Класс символов (character class) — это элемент в образце,который может соответствовать любому символу из заданногомножества. Например, класс %d соответствует любой цифре. Такимобразом, вы можете искать дату в формате dd/mm/yyyy при помощиобразца'%d%d/%d%d/%d%d%d%d':

s="Deadlineis30/05/1999,firm"

date="%d%d/%d%d/%d%d%d%d"

print(string.sub(s,string.find(s,date)))-->30/05/1999

Следующаятаблицасодержитсписоквсехклассовсимволов:.всесимволы%aбуквы%cуправляющиесимволы%dцифры%gпечатныесимволыкромепробельных%lстрочныебуквы%pсимволыпунктуации%sпробельныесимволы%uзаглавныебуквы

Page 249: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

%wбуквенно-цифровыесимволы%xшестнадцатеричныецифрыЛюбаяизэтихбукввверхнемрегистрепредставляетсобойдополнениекласса, т.е. множество не входящих в него символов. Например, '%А'соответствуетвсемнебуквеннымсимволам:

print(string.gsub("hello,up-down!","%A","."))

-->hello..up.down.4

(Число 4 не являются частью итоговой строки. Это второе значение,возвращаемоеgsub,—полноечисловыполненныхзамен.Вдальнейшихпримерахсвыводомрезультатаgsubябудуопускатьэточисло.)

У некоторых символов, называемыхмагическими символами (magiccharacter), есть особое значение при использовании в образце.Магическимисимволамиявляются

().%+-*?[]^$

Символ '%' работает для этих магических символов как экран. Такимобразом, '%.' соответствует точке, а '%%' соответствует самому символу'%'. Вы можете применять экранирующий '%' не только с магическимисимволами, но и с любыми символами, отличными от буквенно-цифровых.Когдасомневаетесь,нерискуйтеииспользуйтеэкран.

Для парсера Lua образцы являются обычными строками. К ним нетособого отношения — они подчиняются тем же правилам, что иостальные строки. Только функции сопоставления с образцомрассматриваютихкакобразцы,итолькоэтифункциисчитаютсимвол'%'экраном. Для помещения кавычек внутрь образца используются те жесамые приемы, что и для других строк; например, вы можетеэкранироватькавычкиприпомощи'\',которыйявляетсяэкранирующимсимволомвLua.

Множество символов (char-set) позволяет вам создавать вашисобственные классы символов, группируя различные классы иодиночные символы внутри квадратных скобок. Например, множество'[%w_]' соответствует как буквенно-цифровым символам, так и символуподчеркивания; множество '[01]' соответствует двоичным цифрам; амножество '[%[%]]' соответствует квадратным скобкам. Для подсчетагласныхвтекстевыможетенаписать

nvow=select(2,string.gsub(text,"[AEIOUaeiou]",""))

При этом вы также можете включать во множества символов

Page 250: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

диапазоны, записывая первый и последний символы с дефисом междуними. Я редко пользуюсь данным средством, поскольку все наиболееупотребительные диапазоны уже предопределены; например, '[0-9]' —этотожесамое,чтои '%d',а '[0-9a-fA-F]'—этотожесамое,чтои '%х'Однако,есливампотребуетсянайтивосьмеричнуюцифру,товыможетепредпочесть '[0-7]' вместо явного перечисления '[01234567]'. Вы такжеможете получить дополнение любого множества символов, поставивперед ним '^': так, образец '[^0-7]' находит любой символ, который неявляетсявосьмеричнойцифрой,а'[^\n]'соответствуетлюбомусимволу,отличному от перевода строки. Но не забывайте, что вы можетеполучить дополнение для простых классов посредством их версии вверхнемрегистре:'%S'проще,чем'[^%s]'.

Выможетесделатьобразцыещеудобнееспомощьюмодификаторовдля повторений и необязательных частей. Образцы в Lua предлагаютчетыретакихмодификатора:+1илиболееповторений*0илиболееповторений-0илиболеекороткихповторений?0или1вхождение(необязательныйобразец)Модификатор '+' соответствует одному или более символампервоначального класса. Он всегда возвращает самую длиннуюпоследовательность символов, которая соответствует образцу.Например,образец'%а+'означаетоднуилинесколькобукв,тоестьслово:

print(string.gsub("one,andtwo;andthree","%a+","word"))

-->word,wordword;wordword

Образец '%d+' соответствует одной или нескольким цифрам(целочисленномунумералу):

print(string.match("thenumber1298iseven","%d+"))-->1298

Модификатор '*' похож на '+' , но при этом он допускает нулевоечисловхожденийсимволовзаданногокласса.Обычноиспользуетсядляобозначения необязательных пробелов между частями образца.Например,длясопоставленияспаройпустыхкруглыхскобок,например() или ( ), можно использовать образец '%(%s*%)': образец '%s*'соответствует нулю и более пробелов. (У круглых скобок также естьособое значение в образцах, поэтому мы должны их экранировать.) Вкачестве другого примера образец '[_%а] [_%w]*' соответствует

Page 251: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

идентификаторам внутри программынаLua: начинается с пробела илисимволаподчеркивания,закоторымидетнольилибольшееколичествоподчеркиванийибуквенно-цифровыхсимволов.

Подобно '*', модификатор '-' также соответствует нулю илибольшему количеству символов заданного класса. Однако, вместосоответствия самой длинной последовательности он соответствуетсамойкороткой.Иногдамежду'*'и '-'нетникакойразницы,нообычноони дают совершенно разные результаты. Например, если выпопытаетесь найти идентификатор при помощи образца '[_%а] [_%а]-',то вы получите лишь первую букву, поскольку '[_%w]-' всегдасоответствует пустой последовательности. С другой стороны,предположим,чтовыхотитенайтикомментариивпрограммеС.Многиесперва пробуют '/%*.*%*/' (то есть '/*', за которыми следуетпоследовательностьлюбыхсимволов,закоторойследует"*/",ивсеэтоссоответственнымиэкранами).Однако,поскольку '.*'. длится столько,сколько сможет, первые комбинация "/*" в вашей программе закроетсялишьсамойпоследней"*/":

test="intx;/*x*/inty;/*y*/"

print(string.match(test,"/%*.*%*/"))

-->/*x*/inty;/*y*/

Образец '.-' наоборот захватит наименьшее количество символов,необходимое для нахождения первого сочетания '*/', и даст, такимобразом,желаемыйрезультат:

test="intx;/*x*/inty;/*y*/"

print(string.gsub(test,"/%*.-%*/",""))

-->intx;inty;

Последниймодификатор'?'соответствуетнеобязательномусимволу.Например, предположим, что мы хотим найти целое число в тексте,котороеприэтомможетсодержатьнеобязательныйзнак.Образец'[+-]?%d+'справляетсясработой,находятакиепоследовательности,как"-12","23" и "+1009". Класс '[+-]' соответствует либо знаку '+', либо знаку '-';следующийзаним'?'делаетэтотзнакнеобязательным.

В отличие от других систем, в Lua модификатор может бытьприменен только к классу символов; группировать образцы под одниммодификаторомнельзя.Например,вLuaнетобразца,соответствующегонеобязательномуслову(еслитольковынеищетеоднобуквенныеслова).Обычно это ограничение можно обойти при помощи продвинутыхприемов,которыемырассмотримвконцеданнойглавы.

Page 252: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Если образец начинается с символа '^', то он будет сопоставлятьсятолькосначаломобрабатываемойстроки.Иточнотакже,еслиобразецзаканчиваетсясимволом'$',тоонбудетсопоставлятьсятолькосконцомобрабатываемой строки. Вы можете использовать эти две метки длясозданияобразцов.Например,следующийтестпроверяет,начинаетсялистрокасцифры:

ifstring.find(s,"^%d")then...

А этот тест проверяет, что строка является целым числом, без другихсимволоввначалеиликонце:

ifstring.find(s,"^[+-]?%d+$")then...

Символы'^'и'$'являютсямагическимилишьтогда,когдаприменяютсяв начале или конце образца. Иначе они служат обычными символами,которыесоответствуютсамимсебе.

Ещеоднимэлементомвобразцеявляется'%b',которыйсоответствуетсбалансированнымстрокам (у которыхпарные символыпо краям).Мызаписываемегокак'%bху',гдехиу—этодваразныхсимвола;символхвыступаеткакоткрывающийсимвол,ау—какзакрывающий.Например,образец '%b()' соответствует частям строки, которыеначинаются с '(' изаканчиваютсянасоответствующей')':

s="a(enclosed(in)parentheses)line"

print(string.gsub(s,"%b()",""))-->aline

Обычномыиспользуемэтотобразецввиде'%b()','%b[]','%b{}'или'%b<>',новыможетеиспользоватьв качестверазделителейотличныедруготдругасимволы.

Наконец, элемент '%f[множество_символов]' является пограничнымобразцом(frontierpattern).Онсоответствуетпустойстроке,толькоеслиследующий символ входит вомножество_символов, а предыдущий—нет:

s="theanthemisthetheme"

print(s:gsub("%f[%w]the%f[%W]","one"))

-->oneanthemisonetheme

Образец '%f[%w]' соответствует границе между небуквенно-цифровым ибуквенно-цифровымсимволами,аобразец'%f[%w]'соответствуетграницемежду буквенно-цифровым символом и небуквенно-цифровымсимволом. Поэтому данный образец сопоставляет строке "the" только

Page 253: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

целое слово. Обратите внимание, что мы должны писать множествосимволов внутри квадратных скобок даже тогда, когда оно являетсяоднимединственнымклассом.

Позиции перед первым и после последнего символов вобрабатываемой строке трактуются как содержащие нуль-символ(символ с ASCII кодом 0). В предыдущем примере первое "the"начинаетсясграницымеждунуль-символом(невомножестве'[%w]')и't'(вомножестве'[%w]').

Пограничный образец был реализован еще в Lua 5.1, но не былдокументирован.ОфициальнымонсталтольковLua5.2.

21.4.Захваты

Механизм захвата (capture) позволяет образцу выдергивать изобрабатываемой строки те части, которые удовлетворяют частямобразца,вцеляхдальнейшегоиспользования.Выможетеуказатьзахватпосредством записи захватываемых частей образца внутри круглыхскобок.

Когда в образце есть захваты, то функция string.match возвращаеткаждое захваченное значение как отдельный результат; другимисловами,онаразбиваетстрокунаеезахваченныечасти.

pair="name=Anna"

key,value=string.match(pair,"(%a+)%s*=%s*(%a+)")

print(key,value)-->nameAnna

Образец '%a+' задает непустую последовательность букв; образец '%s*'задает возможно пустую последовательность пробелов. Поэтому впримеревышевесьобразецзадаетпоследовательностиизнакравенствавследующемпорядке:буквы,пробелы,знакравенства,пробелы,буквы.У обеих последовательностей букв их образцы заключены в круглыескобки,поэтомуприсоответствиионибудутзахвачены.Нижепохожийпример:

date="Todayis17/7/1990"

d,m,y=string.match(date,"(%d+)/(%d+)/(%d+)")

print(d,m,y)-->1771990

Внутри образца элемент вида '%d', где d — это одиночная цифра,соответствует лишь копии d-ого захвата. Чтобы продемонстрироватьтипичное применение, рассмотрим случай, когда вы хотите внутри

Page 254: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

строки найти подстроку, заключенную в одинарные или двойныекавычки. Вы можете попробовать образец наподобие '["'].-["']', тоестькавычка,закоторойследуетчтоугодно,закоторымследуетдругаякавычка;ноприэтомувасбудутпроблемысострокамивроде"it'sallright".Длярешенияданнойпроблемыможнозахватитьпервуюкавычкуииспользоватьеедлязаданиявторойкавычки:

s=[[thenhesaid:"it'sallright"!]]

q,quotedPart=string.match(s,"([\"'])(.-)%1")

print(quotedPart)-->it'sallright

print(q)-->"

Первый захват— это сам символ кавычки, а второй— это подстрокамеждукавычками(подстрока,удовлетворяющая'.-')

Похожим примером является образец, соответствующий длиннымстрокамвLua:

%[(=*)%[(.-)%]%1%]

Он соответствует символам в следующем порядке: открывающаяквадратнаяскобка,нольилибольшеечислознаковравенства,ещеоднаоткрывающая квадратная скобка, что угодно (содержимое строки),закрывающая квадратная скобка, то же самое количество знаковравенства,ещеодназакрывающаяквадратнаяскобка.Пример:

p="%[(=*)%[(.-)%]%1%]"

s="a=[=[[[something]]]==]]=];print(a)"

print(string.match(s,p))-->=[[something]]]==]

Первый захват— это последовательность знаков равенства (в данномпримерелишьодинзнак);второйзахват—этосодержимоестроки.

Третья область применения захваченных значений — замещающаястрока для gsub. Как и образец, замещающая строка может содержатьэлементы наподобие '%d', которые заменяются на соответствующиезахваты при выполнении подстановки. В частности, элемент '%0'меняется на полное соответствие. (Кстати, '%' в замещающей строкедолжен быть экранирован как '%%'.) В качестве примера, следующаякоманда дублирует каждую букву в строке, добавляя дефис междукопиями:

print(string.gsub("helloLua!","%a","%0-%0"))

-->h-he-el-ll-lo-oL-Lu-ua-a!

Этотпримерменяетместамисоседниесимволы:

Page 255: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(string.gsub("helloLua","(.)(.)","%2%1"))-->ehllouLa

В качестве более полезного примера давайте напишем простойпреобразовательформата,которыйполучаетстрокускомандамивстилеLaTeXипереводитихвформатXML:

\command{sometext}--><command>sometext</command>

Еслинамненужнывложенныекоманды,топоможетследующийвызовstring.gsub:

s=[[the\quote{task}isto\em{change}that.]]

s=string.gsub(s,"\\(%a+){(.-)}","<%1>%2</%1>")

print(s)

-->the<quote>task</quote>isto<em>change</em>that.

(В следующем разделе мы увидим, как обрабатывать вложенныекоманды.)

Ещеодинполезныйпримерудаляетпробелыпокраямстроки:functiontrim(s)

return(string.gsub(s,"^%s*(.-)%s*$","%1"))

end

Обратитевниманиенаразумныйвыборформатаобразцов.Дваякоря('^'и '$') обеспечивают получение всей строки. Поскольку '.-' стараетсявыбрать наименьшее число символов, два образца '%s*' соответствуютвсемпробелампокраям.Такжеобратитевнимание,чтопосколькуgsubвозвращаетдва значения, томыиспользуемкруглыескобкивокругеговызовадляотбрасываниялишнегорезультата(числазамен).

21.5.Замены

Вместострокивкачестветретьегоаргументаstring.gsubмыможемиспользоватьфункциюилитаблицу.Привызовесфункциейstring.gsubвызывает эту функцию каждый раз, когда находит соответствие;аргументами каждого вызова являются захваты, а возвращенноефункцией значение используется в качестве замещающей строки. Привызове с таблицей string.gsub обращается к ней, используя первыйзахват как ключ, а связанное с ним значение как замещающую строку.Еслирезультатомвызоваилиобращенияктаблицеявляетсяnil, тоgsubнепроизводитзаменусовпавшейчасти.

В качестве первого примера следующая функция расширяет

Page 256: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

переменныепутемзаменывнутристрокизначенияпеременной$varnameназначениеглобальнойпеременнойvarname:

functionexpand(s)

return(string.gsub(s,"$(%w+)",_G))

end

name="Lua";status="great"

print(expand("$nameis$status,isn'tit?"))

-->Luaisgreat,isn'tit?

Прикаждомсовпадениис'$(%w+)'(знакдоллара,закоторымследуетимяпеременной)функцияgsub ищет захваченноеимяв глобальной таблице_G; результат заменяет совпавшую часть строки. Когда в таблице нетсоответствующегоключа,заменанепроизводится:

print(expand("$othernameis$status,isn'tit?"))

-->$othernameisgreat,isn'tit?

Есливынеуверенывтом,естьлиуданныхпеременныхстроковыезначения,товыможетепопробоватьприменитькихзначениямtostring.В таком случае в качестве замещающего значения вы можетеиспользоватьфункцию:

functionexpand(s)

return(string.gsub(s,"$(%w+)",function(n)

returntostring(_G[n])

end))

end

print(expand("print=$print;a=$a"))

-->print=function:0x8050ce0;a=nil

Теперь при каждом совпадении с образцом '$(%w+)' функция gsub

вызывает заданную функцию с захваченным именем в качествеаргумента;возвращенноезначениеиспользуетсядлязамены.

В последнем примере мы возвращаемся к нашему преобразователюформата из предыдущего раздела. И вновь мы хотим преобразовыватькомандывстилеLaTeX(\example{text})вкомандывстилеXML(text),нона этот размыразрешимвложенные команды.Следующаяфункцияиспользуетдляэтогорекурсию:

functiontoxml(s)

s=string.gsub(s,"\\(%a+)(%b{})",function(tag,body)

body=string.sub(body,2,-2)--убираетквадратные

скобки

body=toxml(body)--обрабатываетвложенные

Page 257: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

команды

returnstring.format("<%s>%s</%s>",tag,body,tag)

end)

returns

end

print(toxml("\\title{The\\bold{big}example}"))

--><title>The<bold>big</bold>example</title>

КодировкаURL

ДлянашегоследующегопримерамывоспользуемсякодировкойURL(URLencoding), которая применяетсяHTTP для передачи параметров вURL.Кодировкапреобразует специальные символы (наподобие '=', '&' и'+')в '%хх', гдехх—этошестнадцатеричныйкод этих символов.Послеэтогоонаменяетпробелына'+'.Например,даннаякодировказашифрует"а+b = с" как "%2Bb+%3D+c". И наконец, она записывает имя каждогопараметраиегозначениесознакомравенствамеждунимиисоединяетполучившиесяпарыname=valueспомощьюамперсанда('&')междуними.Например,значения

name="al";query="a+b=c";q="yesorno"

будутзакодированыкак"name=al&query=a%2Bb+%3D+c&q=yes+or+no".Теперь допустим, что нам требуется декодировать этот URL и

сохранить каждое значение в таблицу, проиндексировав его именемсоответственногопараметра.Следующаяфункциявыполняетосновноедекодирование:

functionunescape(s)

s=string.gsub(s,"+","")

s=string.gsub(s,"%%(%x%x)",function(h)

returnstring.char(tonumber(h,16))

end)

returns

end

Первый оператор заменяет каждый '+' в строке на пробел. Второй gsubсопоставляет все шестнадцатеричные нумералы из двух цифр, передкоторыми стоит '%' и для каждого совпадения вызывает анонимнуюфункцию. Эта функция преобразует шестнадцатеричный нумерал вчисло (tonumber по основанию 16) и возвращает соответствующийсимвол(string.char).Например:

Page 258: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(unescape("a%2Bb+%3D+c"))-->a+b=c

Для декодирования пap name=value мы воспользуемся gmatch.Посколькуиимя,изначениенемогутсодержать'&'или'=',томыможемсопоставитьихприпомощиобразца'[^&=]+':

cgi={}

functiondecode(s)

forname,valueinstring.gmatch(s,"([^&=]+)=([^&=]+)")do

name=unescape(name)

value=unescape(value)

cgi[name]=value

end

end

Вызовgmatch сопоставляет всепарывидаname=value. Для каждой парыитератор возвращает соответствующие захваты (отмеченные круглымискобками в образце) как значения для name и value. Тело цикла простовызываетunsecapeдляобеихстрокизаписываетэтупарувтаблицуcgi.

Также легко записать и соответствующее кодирование. Для началамы напишем функцию escape; эта функция кодирует все специальныесимволы как '%', за которым следует шестнадцатеричный код символа(функция format с опцией "%02Х" создает шестнадцатеричное число издвухцифр,используя0какзаполнитель),азатемменяетпробелына'+':

functionescape(s)

s=string.gsub(s,"[&=+%%%c]",function(c)

returnstring.format("%%%02X",string.byte(c))

end)

s=string.gsub(s,"","+")

returns

end

Функция encode обходит всю таблицу, которую нужно закодировать,строяприэтомитоговуюстроку:

functionencode(t)

localb={}

fork,vinpairs(t)do

b[#b+1]=(escape(k).."="..escape(v))

end

returntable.concat(b,"&")

end

t={name="al",query="a+b=c",q="yesorno"}

print(encode(t))-->q=yes+or+no&query=a%2Bb+%3D+c&name=al

Page 259: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Разложениесимволовтабуляциинапробелы

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

print(string.match("hello","()ll()"))-->35

(Обратите внимание, что результат этого примера отличается отрезультата вызова string.find, поскольку позиция второго пустогозахватаследуетпослесовпадениясобразцом.)

Прекрасным примером использования позиционных захватовявляетсяразложениесимволовтабуляциивстрокенапробелы:

functionexpandTabs(s,tab)

tab=tabor8--tab"size"(defaultis8)

localcorr=0

s=string.gsub(s,"()\t",function(p)

localsp=tab-(p-1+corr)%tab

corr=corr-1+sp

returnstring.rep("",sp)

end)

returns

end

Образец gsub находит все символы табуляции внутри строки,захватывая их позиции. Для каждого символа табуляции внутренняяфункция использует эту позицию, чтобы вычислить количествопробелов, которое нужно, чтобыполучить столбец, кратный значениюtab: сначала она вычитает единицу из этой позиции, чтобы выставитьначало в ноль, а затем добавляет corr для компенсации предыдущихсимволов табуляции (замена каждого символа табуляции влияет напозицию последующих символов). Затем функция вычисляет поправкудляследующегосимволатабуляции:вычитаетединицудляудаляемоготаба и прибавляет sp для добавляемых пробелов. Наконец, онавозвращаетстрокуссоответствующимчисломпробелов.

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

Page 260: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

иметкисимволомтабуляции:functionunexpandTabs(s,tab)

tab=tabor8

s=expandTabs(s)

localpat=string.rep(".",tab)

s=string.gsub(s,pat,"%0\1")

s=string.gsub(s,"+\1","\t")

s=string.gsub(s,"\1","")

returns

end

Эта функция начинает с разложения строки для удаления всехпредыдущих символов табуляции. Затем она вычисляетвспомогательный образец для сопоставления с ним всехпоследовательностей длиной tab символов, и использует этот образецдля добавления метки (управляющего символа \1) после каждойпоследовательностидлинойtab символов.Далеефункция заменяет всепоследовательности пробелов, за которыми следует метка, символамитабуляции. И наконец, она удаляет все оставшиеся метки (те, что безпробеловпередними).

21.6.Специфическиеприемы

Сопоставлениесобразцом—этомощныйинструментдляработысостроками. Вы можете выполнить множество сложных действий всегонесколькими вызовами string.gsub. Однако, как и любой другоймощныйинструмент,ееследуетприменятьаккуратно.

Сопоставление с образцом не заменит полноценный парсер. Дляпрограммна скоруюруку, выможете воспользоватьсяимдляудобнойобработки исходного кода, но получить качественный продукт такимобразом будет тяжело. В качестве хорошего примера рассмотримобразец,которыймыиспользовалидлясопоставленияскомментариямив программе С: '/%*.-%*/'. Если в вашей программе есть строковыйлитерал,содержащий"/*",товыможетеполучитьневерныйрезультат:

test=[[chars[]="a/*here";/*atrickystring*/]]

print(string.gsub(test,"/%*.-%*/","<COMMENT>"))

-->chars[]="a<COMMENT>

Строкисподобнымсодержимымдовольноредки,идлявашихличныхцелейподобныйобразец,скореевсего,будетработать.Новынедолжныраспространятьпрограммустакимнедостатком.

Page 261: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Обычно сопоставление с образцом в Lua довольно эффективно:моему старому Pentium нужно менее 0.3 секунды, чтобы провестисопоставлениесовсемисловамивтекстеразмером4.4Мб(850Кслов).Новыможетепредпринятьмерыпредосторожности.Вывсегдадолжныделать образец как можно более точным, так как неточные образцымедленнее точных. Крайне простой пример — образец '(.-)%$' дляполучения всего текста до первого знака доллара. Если вобрабатываемой строке есть знак доллара, то все пройдет гладко; нопредположим, что в строке вообще нет ни одного знака доллара.Алгоритм попытается найти соответствие образцу, начав с первойпозиции строки. В поисках доллара он пробежит всю строку. Когдастроказакончится,алгоритмдастсбойдляпервойпозициистроки.Затемалгоритмвыполнитполностьюновыйпоиск,начав со второйпозициистроки, и обнаружит, что здесь тоже нет соответствия образцу, и т. д.Этовыльетсявквадратичнуюзависимостьотвремени,занимаяболее4минутнамоемстаромPentiumдлястрокииз100Ксимволов.Выможетелегко исправить данную проблему, привязав образец к первой позициистроки при помощи '^(.-)%$'. С этой привязкой образец будетсопоставлензасотуюдолюсекунды.

Также обращайте внимание на пустые образцы, то есть образцы,которым соответствует пустая строка. Например, если вы попробуетенайти имена при помощи образца наподобие '%а*', то вы везде будетенаходитьихповсюду:

i,j=string.find(";$%**#$hello13","%a*")

print(i,j)-->10

В этом примере вызов string.find правильно находит пустуюпоследовательностьбукввначалестроки.

Всегда бессмысленно писать образец, который начинается илизаканчивается модификатором '-', поскольку ему будет удовлетворятьлишь пустая строка. Этому модификатору обычно требуется что-товокруг него, чтобы уменьшить область его соответствия. Образцы,включающие в себя '.*', тоже довольно коварны, поскольку этаконструкцияможетвобратьбольшесимволов,чемвыпланировали.

ИногдаудобнееиспользоватьсамLuaдляпостроенияобразцов.Мыуже пользовались данным приемом в нашей функции дляпреобразования пробелов в символы табуляции. В качестве другогопримера давайте рассмотрим, как мы можем найти строки текстадлиной, скажем, от 70 символов. Точнее длинной строкой будет

Page 262: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

последовательность из 70 или более символов, отличных от переводастроки. Мы можем найти соответствие одиночному символу, неявляющемуся переводом строки, при помощи класса символов '[^\n]'.Таким образом, мы можем найти соответствие длинной строкепосредствомобразца,который70разповторяетобразецдляодиночногосимвола,ипослекоторогоможетследовать0илиболееэтихсимволов.Вместонаписанияданногообразцавручную,мыможемсоздатьегоприпомощиstring.rep:

pattern=string.rep("[^\n]",70).."[^\n]*"

В качестве другого примера предположим, что вы хотите сделатьпоискбезучетарегистра.Каквариантможнозаменитькаждуюбуквухвобразценакласс'[хХ]',тоестькласс,включающийвсебяистрочную,ипрописную версии буквы. Мы можем автоматизировать этопреобразованиеприпомощифункции:

functionnocase(s)

s=string.gsub(s,"%a",function(c)

return"["..string.lower(c)..string.upper(c).."]"

end)

returns

end

print(nocase("Hithere!"))-->[hH][iI][tT][hH][eE][rR][eE]!

Иногдавамтребуетсязаменитькаждоевхождениеs1наs2,безучетавсякихмагическихсимволов.Еслиобестрокиявляютсялитералами,топри их написании вы можете сами добавить все необходимые экраныдля магических символов. Но если эти строки являются значениямипеременных, то вы можете использовать еще один gsub для вставкиэкрановзавас:

s1=string.gsub(s1,"(%W)","%%%1")

s2=string.gsub(s2,"%%","%%%%")

Встрокепоискамыэкранируемвсенебуквенно-цифровыесимволы(тоестьзаглавную"W").Взамещающейстрокемыэкранируемлишь'%'.

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

Page 263: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

followsatypicalstring:"Thisis\"great\"!".

Одним из решений в подобных случаях является предварительнаяобработка текста такимобразом, чтобы закодироватьпроблематичнуюпоследоватльностьвочто-тоеще.Например,мымоглибызакодировать"\"" как "\1". Тем не менее, если в исходном тексте уже содержалсясимвол"\1",томывбеде.Простымспособомвыполнитькодированиеиизбежать данной проблемы является преобразование всехпоследовательностей " \х" в " \ddd", где ddd — это десятичноепредставлениесимволах:

functioncode(s)

return(string.gsub(s,"\\(.)",function(x)

returnstring.format("\\%03d",string.byte(x))

end))

end

Теперь любая последовательность "\ddd" всегда будет результатомкодирования, поскольку любая "\ddd" в исходной строке тожекодируется.Поэтомудекодированиеявляетсяпростойзадачей:

functiondecode(s)

return(string.gsub(s,"\\(%d%d%d)",function(d)

return"\\"..string.char(tonumber(d))

end))

end

Теперь мы можем завершить нашу задачу. Так как закодированнаястрока не содержит никаких экранированных кавычек ("\""), то мызапростоможемнайтистрокивкавычкахприпомощи'".-"':

s=[[followsatypicalstring:"Thisis\"great\"!".]]

s=code(s)

s=string.gsub(s,'".-"',string.upper)

s=decode(s)

print(s)-->followsatypicalstring:"THISIS\"GREAT\"!".

Иливболеекомпактнойзаписи:print(decode(string.gsub(code(s),'".-"',string.upper)))

21.7.Юникод

На данный момент строковая библиотека не предлагает явнойподдержкиЮникода.Однако,выполнятьнекоторыеполезныеипростыепреобразования строк Юникода в кодировке UTF-8 можно и без

Page 264: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

дополнительныхбиблиотек.UTF-8—этоосновнаякодировкадляЮникодавИнтернете.Из-заее

совместимостисASCIIэтакодировкаидеальноподходитдляLua.Этойсовместимости достаточно, чтобы обеспечить работу с UTF-8 рядатехнологийдляобработкистрокASCIIбезвнесенияизменений.

UTF-8 представляет каждый символ Юникода различным числомбайт. Например, символ 'А' он представляет одним байтом равным 65;буква алеф из иврита, у которой в Юникоде код 1488, представленадвухбайтовой последовательностью 215-144. UTF-8 представляет всесимволы из ASCII как ASCII, то есть одним байтом со значением,меньшим 128. Все остальные символы представленыпоследовательностями байт, где первый байт лежит в диапазоне [194,244], а последующие байты лежат в диапазоне [128, 191]. Точнее,диапазонпервогобайтадлядвухбайтовыхпоследовательностей—[194,223], для трехбайтовых последовательностей — [224, 239] а длячетырехбайтовых последовательностей— [240, 244]. Эта конструкцияследит за тем, чтобы кодовая последовательность одного символаникогда не встретилась внутри кодовой последовательности другого.Например, байт меньше 128 никогда не встретится в многобайтовойпоследовательности;онвсегдапредставленсвоимASCII-символом.

Так как Lua не использует нуль-символы, он может читать,записывать и хранить строки UTF-8 как любые обычные строки.СтроковыелитералытожемогутсодержатьвнутрисебяданныевUTF-8.(Разумеется,выможетередактироватьисходныйкодкакфайлвUTF-8,если захотите.) Операция конкатенации корректно работает для всехстрок в UTF-8. Операции сравнения строк (меньше, чем; меньше илиравно и т. п.) сравнивают строки в UTF-8, следуя порядку символов вЮникоде.

Библиотека операционной системы и библиотека ввода-вывода посути являются интерфейсами к системе, на которой они выполняются,поэтомуихподдержкастроквUTF-8зависитотэтойсистемы.ВLinux,например,мыможемиспользоватьUTF-8дляименфайлов,ноWindowsиспользуетUTF-16.ПоэтомудляработысименамифайловвЮникодевWindowsпонадобятсялибодополнительныебиблиотеки,либовнесениеизмененийвстандартныебиблиотекиLua.

Теперь давайте посмотрим, как функции из строковой библиотекиработаютсострокамивUTF-8.

Функции string.reverse, string.byte, string.char, string.upper иstring.lower не работают со строками в UTF-8, поскольку все они

Page 265: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

полагают,чторазмеродногосимволаравенодномубайту.Функции string.format и string.rep работают со строками в UTF-8

без проблем, за исключением опции форматирования '%с', котораясчитает, что один символ — это один байт. Функции string.len иstring.sub корректно работают со строками в UTF-8, но при этоминдексыуказываютнаколичествобайт(анесимволов).Чащевсегоэтоименното,чтовамнужно.Но,какмыскороувидим,мытакжеможемпосчитатьиколичествосимволов.

ДляфункцийсопоставлениясобразцамиихприменимостькстрокамвUTF-8зависитотобразца.Простыеобразцыработаютбезкаких-либопроблем в связи с ключевым свойствомUTF-8— код одного символаникогданеможетнаходитьсявнутрикодадругого.Классыимножествасимволов работают лишь для ASCII-символов. Например, образец '%s'работает для строк в UTF-8, но он будет соответствовать толькопробелам ASCII и не будет соответствовать дополнительнымпробельным символам в Юникоде, таким как неразрывный пробел(U+00A0), разделитель параграфов (U+2029) или монгольскийразделительгласных(U+180Е).

НекоторыеобразцымогутудачноиспользоватьособенностиUTF-8.Например, если вы хотите посчитать число символов в строке, то выможетеиспользоватьследующеевыражение:

#(string.gsub(s,"[\128-\191]",""))

Этот gsub убирает продолжающие байты из строки, чтобы осталисьлишь однобайтовые последовательности и начальные байтымногострочныхпоследовательностей:одинбайтнакаждыйсимвол.

Взяв за основу похожие идеи, следующий пример показывает, какможноперебратьвсесимволывстрокевUTF-8:

forcinstring.gmatch(s,".[\128-\191]*")do

print(c)

end

Листинг 21.1 иллюстрирует некоторые приемыдля работы сUTF-8строками в Lua. Разумеется, для выполнения этих примеров вам нужнаплатформа,вкоторойprintподдерживаетUTF-8.

К сожалению, больше Lua предложить нечего. Полноценнаяподдержка Юникода требует огромных таблиц, которые плохосоотносятся с маленьким размером Lua. У Юникода слишком многоособенностей. Практически невозможно абстрагировать какое-либо

Page 266: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

понятиеизконкретныхязыков.Размытодажепонятиетого,чтоявляетсясимволом, поскольку нет взаимно-однозначного соответствия междузакодированными в Юникоде символами и графемами (то естьсимволамисдиакритическимизнакамии«полностьюигнорируемыми»символами).Другие,казалосьбы,фундаментальныепонятия,например,того,чтоявляетсябуквой,такжеразнятсясредиязыков.

Чего, на мой взгляд, не хватает в Lua, так это функцийпреобразования последовательностей UTF-8 и комбинаций Юникодамежду собой и функций для проверки правильности строк в UTF-8.Возможно,онивойдутвследующуюверсиюLua.Длятех,комунужнаподдержка Юникода, похоже лучшим вариантом будет использованиевнешнейбиблиотекивродеslnunicode.

Листинг21.1.ПримерыбазовыхоперацийнадUTF-8вLualocala={}

a[#a+1]="Nähdään"

a[#a+1]="ação"

a[#a+1]="ÃøÆËÐ"

locall=table.concat(a,";")

print(l,#(string.gsub(l,"[\128-\191]","")))

-->Nähdään;ação;ÃøÆËÐ18

forwinstring.gmatch(l,"[^;]+")do

print(w)

end

-->Nähdään

-->ação

-->ÃøÆËÐ

forcinstring.gmatch(a[3],".[\128-\191]*")do

print(c)

end

-->Ã

-->ø

-->Æ

-->Ë

-->Ð

Упражнения

Упражнение21.1.Напишитефункциюsplit,котораяполучаетстроку

Page 267: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

и образец разделителя и возвращает последовательность из частейисходнойстрокимеждуразделителями:

t=split("awholenewworld","")

--t={"a","whole","new","world"}

Каквашафункцияобрабатываетпустыестроки?(Вчастности,являетсяли пустая строка пустой последовательностью илипоследовательностьюсоднойпустойстрокой?)

Упражнение21.2.Образцы '%D' и '[A%d]' эквивалентны.А что насчетобразцов'[^%d%u]'и'[%D%U]'?

Упражнение21.3.Напишитефункциютранслитерации.Этафункцияполучает строку и заменяет каждый символ в этой строке другимсимволом в соответствии с таблицей, заданной в качестве второгоаргумента. Если таблица отображает 'а' в 'b', то функция должназаменитькаждоевхождение'а'на'b'.Еслитаблицаотображает'а'вfalse,тофункциядолжнаудалитьвсевхождениясимвола'а'изстроки.

Упражнение 21.4. Напишите функцию, которая переворачиваетстрокувUTF-8.

Упражнение21.5.НапишитефункциютранслитерациидлясимволовUTF-8.

Page 268: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА22

Библиотекаввода-вывода

Библиотека ввода-вывода предлагает две различные модели дляработы с файлами. В простой модели используется текущий входнойфайл (current input file)итекущий выходнойфайл (current output file), иона производит над ними операции ввода-вывода. Полная модельиспользует явные дескрипторы файлов; она опирается на объектно-ориентированныйподход,которыйопределяетвсеоперациикакметодынаддескрипторамифайлов.

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

22.1.Простаямодельввода-вывода

Простаямодель выполняет все своиоперациинад двумя текущимифайлами. Библиотека инициализирует текущий входной файл какобрабатываемый стандартный ввод (stdin), а текущий выходной файлкакобрабатываемыйстандартныйвывод(stdout).Такимобразом,когдамы выполняем что-то вроде io.read(), мы читаем строку изстандартноговвода.

Мы можем изменить эти текущие файлы при помощи функцийio.input и io.output. Вызов наподобие io.input(filename) открываетзаданный файл в режиме чтения и устанавливает его как текущийвходнойфайл.Начиная с этогомомента, весь вводбудетпоступатьизэтого файла, пока не произойдет другой вызов io.input; функцияio.output делает то же самое, но для вывода. В случае ошибки обефункцииеевызывают.Есливыхотитеобрабатыватьошибкинапрямую,товыдолжныиспользоватьполнуюмодель.

Функция write проще, чем read, поэтому мы сперва рассмотрим ее.Функцияio.writeполучаетпроизвольноечислостроковыхаргументовизаписывает их в текущий выходной файл. Она преобразует числа встроки, следуя стандартным правилам преобразования; для полногоконтроля над этим преобразованием используйте функцию

Page 269: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

string.format:>io.write("sin(3)=",math.sin(3),"\n")

-->sin(3)=0.14112000805987

>io.write(string.format("sin(3)=%.4f\n",math.sin(3)))

-->sin(3)=0.1411

Избегайте кода вродеio.write(a..b..с); вызовio.write(a,b,c) дает тотжеэффектсменьшимизатратами,посколькуприэтомконкатенацияненужна.

Какправило,выдолжныиспользоватьprintдляпрограммнаскоруюрукуилидляотладки,аwriteприменятьтогда,когдавамнуженполныйконтрольнадвыводом:

>print("hello","Lua");print("Hi")

-->helloLua

-->Hi

>io.write("hello","Lua");io.write("Hi","\n")

-->helloLuaHi

В отличие от print, функция write не добавляет к выводу никакихдополнительных символов вроде символов табуляции или переводовстроки. Кроме того, write позволяет вам перенаправить ваш вывод,тогда как print всегда использует стандартный вывод. Наконец, printавтоматическиприменяетtostringксвоимаргументам;этоудобнодляотладки,номожетскрыватьошибки,есливыневнимательныквыводу.

Функция io.read читает строки из текущего входного файла. Ееаргументыуправляюттем,чточитать:*ачитаетвесьфайл*lчитаетследующуюстроку(безсимволапереводастроки)*Lчитаетследующуюстроку(ссимволомпереводастроки)*nчитаетчислочислоограничиваетколичествочитаемыхсимволов

Вызов io.read("*а") читает весь текущий входной файл, начиная стекущейпозиции.Еслимынаходимся в концефайлаилифайлпуст, товызоввозвращаетпустуюстроку.

ПосколькуLuaэффективноработаетсдлиннымистроками,простойприемнаписанияфильтровнаLuaсостоитвтом,чтобыпрочестьвесьфайлвстроку,выполнитьобработкуэтойстроки(обычноприпомощиgsub)изатемзаписатьстрокуввывод:

t=io.read("*a")--читаетизфайла

Page 270: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

t=string.gsub(t,...)--делаетсвоюработу

io.write(t)--пишетвфайл

ВкачествепримераследующийкусокявляетсязаконченнойпрограммойдлякодированиясодержимогофайлавMIME-кодировкеquoted-printable.Каждый не ASCII-байт кодируется как =хх, где хх — этошестнадцатеричное значение байта. Для целостности кодированиясимвол'='такжедолженбытьзакодирован:

t=io.read("*a")

t=string.gsub(t,"([\128-\255=])",function(c)

returnstring.format("=%02X",string.byte(c))

end)

io.write(t)

Этотобразец,использованныйвgsub, захватываетвсебайтыот128до255,атакжезнакравенства.

Вызов io.read ("*l") возвращает следующую строку из текущеговходного файла без символа перевода строки; вызов io.read ("*L")

аналогичен, но он оставляет символ перевода строки (если он был вфайле).Когдамыдостигаемконцафайла,вызоввозвращаетnil(таккакнетследующейстрокидлявозврата).Поумолчаниювreadиспользуется"*l". Обычно я использую этот образец, только когда алгоритместественным образом обрабатывает файл строка за строкой; впротивномслучаеяпредпочитаюпрочестьвесьфайлзаразприпомощи"*а"иличитатьегоблоками,какмыувидимпозже.

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

forcount=1,math.hugedo

localline=io.read()

ifline==nilthenbreakend

io.write(string.format("%6d",count),line,"\n")

end

Однако, чтобы построчно перебрать весь файл, лучше использоватьитератор io.lines. Например, мы можем написать законченнуюпрограммудлясортировкистрокфайласледующимобразом:

locallines={}

--считываетстрокивтаблицу'lines'

forlineinio.lines()dolines[#lines+1]=lineend

--сортирует

table.sort(lines)

Page 271: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

--записываетвсестроки

for_,linipairs(lines)doio.write(l,"\n")end

Вызовio.read("*n") читает число из текущего входногофайла.Этоединственныйслучай,когдафункцияreadвозвращаетчисло,анестроку.Когда программе нужно прочесть слишком много чисел из файла,отсутствие промежуточных строк улучшает ее быстродействие. Опция*n пропускает все пробелы перед числом и поддерживает такиечисловыеформаты,как-3,+5.2,1000и-3.4е-23.Еслифункциянеможетнайти число в текущей позиции (из-за неверного формата или концафайла),тоонавозвращаетnil.

Вы можете вызвать read с несколькими опциями; для каждогоаргументафункция вернет соответствующее значение.Допустим, у васестьфайл,содержащийпотричисланакаждуюстроку:

6.0-3.2315e12

4.32341000001

...

Теперь вы хотите напечатать максимальное число каждой строки. Выможетепрочестьвсетричислазаодинвызовread:

whiletruedo

localn1,n2,n3=io.read("*n","*n","*n")

ifnotn1thenbreakend

print(math.max(n1,n2,n3))

end

Кроме основных образцов для чтения, вы можете вызвать read саргументом в виде числа n: в этом случае read пытается прочесть nсимволов из входного файла. Если она не может прочесть ни одногосимвола (конец файла), то она возвращает nil; в противном случаевозвращается строка с не более чем n символами. В качестве примераданного образца для чтения следующая программа показываетэффективный способ (для Lua, конечно) скопировать файл из stdin вstdout:

whiletruedo

localblock=io.read(2^13)--размербуфераравен8K

ifnotblockthenbreakend

io.write(block)

end

Как особый случай, io.read(0) работает как проверка конца файла:онавозвращаетлибопустуюстроку,еслиещеесть,чточитать,либоnil,

Page 272: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

есличитатьнечего.

22.2.Полнаямодельввода-вывода

Для большего контроля над вводом-выводом вы можетеиспользовать полную модель. Данная модель основана на понятиидескрипторафайла(filehandle),которыйаналогиченпотокам(FILE*)вС:онпредставляетсобойоткрытыйфайлстекущейпозицией.

Чтобы открыть файл, вы используете функцию io.open, котораяподобнафункцииfopen вС.Вкачестве аргументовонапринимаетимяфайла, который нужно открыть, и строку режима (mode string). Этастрокаможетсодержать'r'длячтения,'w'длязаписи(котораяприэтомстираетлюбоепредыдущеесодержимоефайла)или'a'длядобавлениякфайлу; еще она может содержать необязательный 'b' для открытиядвоичныхфайлов.Функцияopenвозвращаетновыйдескрипторфайла.Вслучаеошибкиopen возвращаетnil, а такжесообщениеобошибкеиеекод:

print(io.open("non-existent-file","r"))

-->nilnon-existent-file:Nosuchfileordirectory2

print(io.open("/etc/passwd","w"))

-->nil/etc/passwd:Permissiondenied13

Интерпретациякодовошибокзависитотсистемы.Типичнаяидиомадляпроверкинаошибки:localf=assert(io.open(filename,mode))

Если open даст сбой, то сообщение об ошибке переходит вторымаргументомвassert,котораязатемпоказываетэтосообщение.

Послеоткрытияфайлавыможетчитатьизнегоиписатьвнегоприпомощиметодовreadиwrite.Онианалогичныфункциямreadиwrite,новывызываетеих какметодыдескрипторафайла, используя двоеточие.Например, чтобы открыть файл и прочесть все из него, вы можетеиспользоватькусоквродеэтого:

localf=assert(io.open(filename,"r"))

localt=f:read("*a")

f:close()

Библиотека ввода-вывода предлагает дескрипторы для трехпредопределенных потоковС: io.stdin, io.stdout и io.stderr. Поэтому

Page 273: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

выможете послать сообщение прямо в поток с ошибкой при помощипримернотакогокода:

io.stderr:write(message)

Мы можем смешивать полную модель с простой. Мы получаемдескриптор текущего входного файла посредством вызова io.input()без аргументов. Мы устанавливаем этот дескриптор путем вызоваio.input(hanle) (аналогичные вызовы работают и для io.output).Например,есливыхотитевременноизменитьтекущийвходнойфайл,товыможетенаписатьчто-товродеэтого:

localtemp=io.input()--сохраняеттекущийфайл

io.input("newinput")--открываетновыйтекущийфайл

<делаетчто-нибудьсновымвводом>io.input():close()--закрываеттекущийфайл

io.input(temp)--восстанавливаетпредыдущийтекущий

файл

Вместо io.read для чтения из файла мы такжеможем использоватьio.lines. Как мы уже видели в предыдущих примерах, iо.lines

возвращаетитератор,последовательночитающийизфайла.Первымаргументомio.linesможетбытьимяфайлаилидескриптор

файла.Еслипереданоимяфайла,тоio.linesоткроетфайлврежимедлячтения и закроет файл после достижения конца файла. Если передандескриптор файла, то io.lines будет использовать данный файл длячтения;вэтомслучаеio.linesнебудетзакрыватьфайлподостиженииегоконца.Вслучаевызовавообщебезаргументовio.linesбудетчитатьданныеизтекущеговходногофайла.

НачинаясLua5.2,io.linesпринимаетитеопции,которыепринимаетio.readпослефайловогоаргумента.Вкачествепримераследующийкодкопируетфайлвтекущийвывод,используяio.lines:

forblockinio.lines(filename,2^13)do

io.write(block)

end

Небольшойприемдляувеличениябыстродействия

ОбычновLuaбыстреепрочестьфайлцеликом,чемчитатьегостроказа строкой. Однако, иногда мы сталкиваемся с большим файлом(например,десяткиилидажесотнимегабайт),читатькоторыйцеликом

Page 274: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

было бы нецелесообразно. Если вы хотите получить максимальноебыстродействие при работе с такими большими файлами, то быстреевсего будет читать его достаточно большими кусками (например, по8К). Во избежание возможного разрыва строки, можно простопопроситьпрочестьещеоднустроку:

locallines,rest=f:read(BUFSIZE,"*l")

Переменная rest получит остаток любой строки, разбитой при чтениикуска. Затем мы объединяем кусок и полученный остаток. Такимобразомкусоквсегдабудетзавершатьсянаграницестрок.

Пример из листинга 22.1 использует этот прием для реализации wc,программы, которая считает число символов, слов и строк в файле.Обратите внимание на использование io.lines для осуществленияитерацийиопции"*b"длячтениястроки—этодоступно,начинаясLua5.2.

Листинг22.1.ПрограммаwclocalBUFSIZE=2^13--8K

localf=io.input(arg[1])--открываетвходнойфайл

localcc,lc,wc=0,0,0--счетчикисимволов,строкислов

forlines,restinio.lines(arg[1],BUFSIZE,"*L")do

ifrestthenlines=lines..restend

cc=cc+#lines

--подсчитываетсловавкуске

local_,t=string.gsub(lines,"%S+","")

wc=wc+t

--подсчитываетпереводыстрокивкуске

_,t=string.gsub(lines,"\n","\n")

lc=lc+t

end

print(lc,wc,cc)

Бинарныефайлы

Функции io.input и io.output из простой модели всегда поумолчаниюоткрываютфайл в текстовомрежиме.ВUNIXнет никакойразницы между бинарными и текстовыми файлами. Но в некоторыхсистемах,вчастностивWindows,бинарныефайлынужнооткрыватьсоспециальным флагом. Для обработки таких бинарных файлов выдолжныиспользоватьio.openссимволом'b'встрокережима.

Luaработаетсбинарнымиданнымитакже,какистекстом.Строкав

Page 275: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Luaможетсодержатьлюбыебайты,ипочтивсефункциивбиблиотекахмогут обрабатывать любые байты. Вы даже можете применятьсопоставлениесобразцомкбинарнымданнымдотехпор,покаобразецне содержит нулевого байта. Если вам нужно сопоставление с этимбайтом,товыможетевоспользоватьсядляэтогоклассом%z.

Обычно бинарные данные читают либо при помощи образца *а,которыйчитаетвесьфайл,либоприпомощиобразцаn,которыйчитаетn байт. В качестве простого примера следующая программа переводиттекст из формата Windows в формат UNIX (то есть заменяетпоследовательность символов перевода каретки и перевода строки насимвол перевода строки). Она не пользуется стандартными файламиввода-вывода (stdin-stdout), поскольку они открыты в текстовомрежиме. Вместо этого она полагает, что имена входного и выходногофайловпереданыпрограммекакаргументы:

localinp=assert(io.open(arg[1],"rb"))

localout=assert(io.open(arg[2],"wb"))

localdata=inp:read("*a")

data=string.gsub(data,"\r\n","\n")

out:write(data)

assert(out:close())

Выможетевызватьэтупрограммуприпомощиследующейкоманднойстроки:

>luaprog.luafile.dosfile.unix

Вкачествеещеодногопримераследующаяпрограммапечатаетвсестроки,найденныевбинарномфайле:

localf=assert(io.open(arg[1],"rb"))

localdata=f:read("*a")

localvalidchars="[%g%s]"

localpattern="("..string.rep(validchars,6).."+)\0"

forwinstring.gmatch(data,pattern)do

print(w)

end

Программа считает, что строка — это завершенная нулемпоследовательностьнеменеечемизшестидопустимыхсимволов, гдедопустимым является любой символ, который соответствует образцуvalidchars. В нашем примере этот образец состоит из печатаемыхсимволов. Мы используем string.rep и конкатенацию для создания

Page 276: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

образца,которомуудовлетворяютпоследовательностиизшестииболеедопустимых символов, за которыми следует нулевой байт. Круглыескобкивобразцезахватываютэтустроку(безнулевогобайта).

В качестве последнего примера следующая программа делает дампбинарногофайла:

localf=assert(io.open(arg[1],"rb"))

localblock=16

forbytesinf:lines(block)do

forcinstring.gmatch(bytes,".")do

io.write(string.format("%02X",string.byte(c)))

end

io.write(string.rep("",block-string.len(bytes)))

io.write("",string.gsub(bytes,"%c","."),"\n")

end

Листинг 22.2. Получение дампа посредством программыdump

6C6F63616C2066203D20617373657274localf=assert

28696F2E6F70656E286172675B315D2C(io.open(arg[1],

202272622229290A6C6F63616C20626C"rb")).localbl

6F636B203D2031360A666F7220627974ock=16.forbyt

657320696E20663A6C696E657328626Cesinf:lines(bl

...

20222C20737472696E672E6773756228",string.gsub(

62797465732C20222563222C20222E22bytes,"%c","."

292C20225C6E22290A656E640A0A),"\n").end..

Как и раньше, первым аргументом программы является имя входногофайла; выходной файл идет на стандартный вывод. Программа читаетфайл кусками по 16 байт. Для каждого куска выводитсяшестнадцатеричное представление каждого байта, а затем кусокзаписываетсякактекст,заменяяуправляющиесимволыточками.

Листинг 22.2 показывает результат применения этой программысамойксебе(наUNIX-машине).

22.3.Прочиеоперациинадфайлами

Функция tmpfile возвращает дескриптор временного файла,открытого в режиме чтения-записи. Этот файл будет автоматическиудаленпозавершениипрограммы.

Функцияflushприменяеткфайлувсеотложенныеоперациизаписи.Подобноwrite,выможетевызватьеелибокакфункциюio.flush()для

Page 277: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

сбросатекущеговыходногофайланадиск,либокакметодf:flush()длясбросаконкретногофайлаf.

Методsetvbufустанавливаетрежимбуферизациипотока.Егопервымаргументом является строка: "no" означает отсутствие буферизации;"full" означает, что поток записывается лишь тогда, когда буферзаполнен или вы явно сбрасываете файл; "line" означает, что выводбуферизуется до перевода строки или при вводе из некоторых особыхфайлов (например, из терминала). Для последних двух опций setvbufдопускаетнеобязательныйвторойаргумент,задающийразмербуфера.

В большинстве систем стандартный поток ошибок (io.stderr) небуферизуется, в то время как стандартный выходной поток (io.stdout)буферизуется в построчном режиме. Поэтому если вы записываетенезавершенные строки в стандартный вывод (например, индикаторсостояния операции), то, чтобы увидеть вывод, вам можетпонадобитьсясброссодержимогобуфера.

Метод seek может как возвращать, так и устанавливать текущуюпозицию внутри файла. Его общей формой являетсяf:seek(место,смещение), гдеместо— это строка, задающая, как надоинтерпретировать смещение. Ее допустимыми значениями являются"set",когдасмещениетрактуетсяотначалафайла,"cur",когдасмещениетрактуется от текущей позиции внутрифайла, и "end", когда смещениетрактуется с конца файла. Независимо от значения места вызоввозвращает новую текущую позицию, измеренную в байтах от началаэтогофайла.

Значениями по умолчанию являются "cur" для места и 0 длясмещения. Поэтому вызов file:seek() возвращает текущее положениевнутрифайла,неменяяего;вызовfile:seek("set")возвращаетпозициюв начало файла (и возвращает ноль); а вызов file:seek("end")

устанавливает позицию на конец файла и возвращает его размер.Следующаяфункцияполучаетразмерфайла,неменяятекущуюпозициювнутринего:

functionfsize(file)

localcurrent=file:seek()--получаеттекущуюпозицию

localsize=file:seek("end")--получаетразмерфайла

file:seek("set",current)--восстанавливаетпозицию

returnsize

end

В случае ошибки все эти функции возвращают nil и сообщение обошибке.

Page 278: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнения

Упражнение 22.1. Напишите программу, которая читает текстовыйфайлиперезаписываетего,используяегожестроки,отсортированныевалфавитномпорядке.Привызовебезаргументовонадолжначитатьизстандартного входного файла и записывать в стандартный выходнойфайл.Привызовесоднимаргументомввидеименифайла,онадолжначитатьизэтогофайлаизаписыватьвстандартныйвыходнойфайл.Привызовесдвумяаргументамиввидеименфайловонадолжначитатьизпервогофайлаиписатьвовторой.

Упражнение22.2.Изменитепредыдущуюпрограммутак,чтобыоназапрашивала подтверждение, если пользователь задает имясуществующегофайладляеевывода.

Упражнение 22.3. Сравните быстродействие программы на Lua,которая копирует стандартныйвходнойфайл в стандартныйвыходнойфайлследующимиспособами:

побайтно;построчно;кускамипо8К;весьфайлзараз.

Насколько большим может быть входной файл для последнеговарианта?

Упражнение 22.4. Напишите программу, которая печатаетпоследнюю строку текстового файла. Постарайтесь избежать чтениявсегофайла,когдафайлбольшойикнемуможноприменитьseek.

Упражнение22.5.Обобщитепредыдущуюпрограммутак,чтобыонапечатала последние n строк текстового файла. Опять же постарайтесьизбежать чтения всего файла, когда он большой и к нему можноприменитьseek.

Page 279: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА23

Библиотекаоперационнойсистемы

Библиотека операционной системы включает в себя функции дляработысфайлами(неспотоками),получениятекущихдатыивремениидругиесредства,касающиесяоперационнойсистемы.Онаопределенавтаблицеos.ПереносимостьLuaсказаласьнаэтойбиблиотеке:посколькуLua написана на чистом ANSI С, то эта библиотека включает в себятолькофункциональность предоставляемую стандартомANSI.МногиесредстваОС,такиекакработасдиректориямиисокетами,невходятвэтот стандарт, и поэтому данная библиотека их не предоставляет.Существуют другие библиотеки Lua, не включенные в основнуюпоставку, которые обеспечивают расширенный доступ кОС.Примерытаких библиотек: posix, предоставляющая для Lua всюфункциональность стандарта POSIX.1, luasocket для работы с сетью иLuaFileSystemдляработысдиректориямииатрибутамифайлов.

Все,чтопредлагаетданнаябиблиотекадляработысфайлами,—этофункцииos.renameдляизмененияименифайлаиos.removeдляудаленияфайла.

23.1.Датаивремя

Вся функциональность для работы с датами и временем в Luaобеспечиваетсядвумяфункциями—timeиdate.

Функция time, когда она вызвана без аргументов, возвращаеттекущую дату и время, представленные как число. (В большинствесистемэточислосекунд,прошедшихсопределенногоначалаотсчета.)При вызове с аргументом в виде таблицы, она возвращает число,которое представляет дату и время, описанное этой таблицей.У такихдата-таблиц(datetables)естьследующиезначимыеполя:yearполныйгодmonth01—12(месяц)day01—31(день)hour00—23(час)min00—59(минута)

Page 280: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

sec00—59(секунда)isdstлогическоезначение,trueприпереходеналетнеевремяПервыетриполяобязательны;дляостальныхзначениемпоумолчанию,когда оно не предоставлено, является полдень (12:00:00). В UNIX-системе, где началом отсчета является 00:00:00 1 января 1970 г. поуниверсальному координированному времени, для Рио-де-Жанейро(который на три часа западнее Гринвича) у нас будут следующиерезультаты:

print(os.time{year=1970,month=1,day=1,hour=0})

-->10800

print(os.time{year=1970,month=1,day=1,hour=0,sec=1})

-->10801

print(os.time{year=1970,month=1,day=1})

-->54000

(Обратитевнимание,что10800—это3часавсекундах,54000—это10800плюс12часоввсекундах.)

Функция date, несмотря на свое имя, является своего родапротивоположностью функции time: она преобразует число,обозначающее дату и время, обратно в какое-нибудь высокоуровневоепредставление. Ее первый параметр — это форматирующая строка,описывающая,какоеименнопредставлениенамнужно.Второйпараметр— это дата и время в виде одного числа; по умолчанию, если его неиспользовать,онравентекущейдатеивремени.

Чтобы получить дата-таблицу, мы воспользуемся форматирующейстрокой "*t". Например, вызов os.date("*t",906000490) вернетследующуютаблицу:

{year=1998,month=9,day=16,yday=259,wday=4,

hour=23,min=48,sec=10,isdst=false}

Обратите внимание, что, кроме полей, используемых os.time, таблица,созданнаяos.date,такжезадаетденьнедели(wday,1—этовоскресенье)иденьгода(yday,1—это1-оеянваря).

Для других форматирующих строк os.date форматирует дату каккопию форматирующей строки, где заданные теги замененыинформацией о дате и времени. Тег состоит из '%', за которым следуетбуква,каквследующихпримерах:

print(os.date("a%Ain%B"))-->aTuesdayinMay

print(os.date("%x",906000490))-->09/16/1998

Page 281: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Все представления соответствуют текущей локали. Например, длябразильской португальской локали результатом %В даст "setembro", а %хдаст"16/09/98".

Следующая таблица показывает каждый тег, его смысл и значениедля 16 сентября 1998 года, 23:48:10 (среда). Для числовых значенийтаблицатакжепоказываетихдиапазондопустимыхзначений:%aсокращенноеназваниеднянедели(например,Wed)%Aполноеназваниеднянедели(например,Wednesday)%bсокращенноеназваниемесяца(например,Sep)%Bполноеназваниемесяца(например,September)%cдатаивремя(например,09/16/9823:48:10)%dденьмесяца(16)[01–31]%Hчас,используя24-часовоевремя23)[00–23]%Iчас,используя12-часовоевремя(11)[01–12]%jденьгода(259)[001–366]%Mминута(48)[00–59]%mмесяц(09)[01–12]%pлибо"am",либо"pm"%Sсекунда(10)[00–60]%wденьнедели(3)[0–6=Sunday–Saturday]%xдата(например,09/16/98)%Xвремя(например,23:48:10)%yсокращенныйгодиздвухцифр(98)[00–99]%Yполныйгод(1998)%%символ'%'

Если вы вызовете date без каких-либо аргументов, то будетиспользован формат %с, то есть полная дата и время в подходящемформате.Обратитевнимание,чтопредставлениядля%х,%Xи%с зависятот локали и системы. Если вам нужно фиксированное представление,например mm/dd/yyyy, то используйте явную строку формата вроде"%m/%d/%Y".

Функцияos.clockвозвращаетчислозатраченныхпрограммойсекундпроцессорного времени. Обычно она используется для замерапроизводительностифрагментакода:

localx=os.clock()

locals=0

fori=1,100000dos=s+iend

print(string.format("elapsedtime:%.2f\n",os.clock()-x))

Page 282: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

23.2.Прочиесистемныевызовы

Функция os.exit завершает выполнение программы. Еенеобязательныйпервый аргумент—это статуспрограммыпри выходеизнее.Онможетбытьчислом (успешномувыполнениюсоответствуетноль) или логическим значением (успешному выполнениюсоответствуетtrue).Еслинеобязательныйвторойаргументравенtrue,тосостояние Lua закрывается посредством вызова всех финализаторов иосвобождения всей памяти, используемой этим состоянием. (Обычноэта финализация не является обязательной, так как большинствооперационныхсистемосвобождаетвсересурсы,занятыепроцессом,привыходеизнего.)

Функцияоs.getenv возвращает значениепеременнойокружения.Онапринимаетимяпеременнойивозвращаетстрокусеезначением:

print(os.getenv("HOME"))-->/home/lua

Длянеопределенныхпеременныхэтотвызоввозвращаетnil.Функцияos.execute выполняеткомандуоперационнойсистемы;она

эквивалентнафункцииsystem вС.Онапринимает строку с командойивозвращаетинформациюотом,какэтакомандабылазавершена.Первоевозвращаемоезначениелогическое: trueозначаетокончаниепрограммыбез ошибок. Второе возвращаемое значение— это строка: "exit", еслипрограмма завершилась нормально, и "signal", если она была прерванасигналом. Третье возвращаемое значение — это статус возврата, еслипрограмма завершилась нормально, или номер сигнала, если оназавершиласьпосигналу.ВкачествепримераиспользованияивWindows,и вUNIX выможете использовать следующуюфункцию для созданияновыхдиректорий:

functioncreateDir(dirname)

os.execute("mkdir"..dirname)

end

Функцияos.executeобладаетширокимивозможностями,ноприэтомсильнозависитотиспользуемойсистемы.

Функция os.setlocale задает текущую локаль, используемуюпрограммой Lua. Локали определяют поведение, которое зависит откультурных и языковых различий. У функции os.setlocale есть двастроковых параметра: имя локали и категория, которая определяет, накакие характеристики эта локаль повлияет. У локалей есть шесть

Page 283: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

возможныхкатегорий:

"collate"управляеталфавитнымпорядкомстрок;"ctype"управляеттипамиотдельныхсимволов(например,определяет,чтоявляетсябуквой)ипреобразованиеммеждустрочнымиизаглавнымибуквами;"monetary"невлияетнапрограммынаLua;"numeric"управляеттем,какформатируютсячисла;"time"управляеттем,какформатируютсядатаивремя(дляфункцииos.date);"all"управляетвсемиперечисленнымифункциями.

Категорией по умолчанию является "all", то есть если вы вызвалиsetlocale только с именем локали, то она будет выставлена для всехкатегорий.Функцияsetlocaleвозвращаетимялокалииnilвслучаесбоя(обычнокогдасистеманеподдерживаетданнуюлокаль).

print(os.setlocale("ISO-8859-1","collate"))-->ISO-8859-1

У категории "numeric" есть некоторые тонкости. Посколькупортугальский и некоторые латинские языки используют дляпредставления десятичных чисел запятую вместо точки, то локальменяетспособ,которымLuaпечатаетисчитываетэтичисла.Нолокальне влияет на то, как Lua разбирает числа внутри программы (одна измногих причин состоит в том, что у выражений вроде print(3,4) ужеестьзначениевLua.ЕсливыпишетенаLuaфрагментыкода,тоздесьувасмогутбытьпроблемы:

print(os.setlocale("pt_BR"))-->pt_BR

s="return("..3.4..")"

print(s)-->return(3,4)

print(loadstring(s))

-->nil[string"return(3,4)"]:1:')'expectednear','

Во избежание подобных проблем, убедитесь, что ваша программаиспользуетстандартнуюлокаль"С"присозданиифрагментовкода.

Упражнения

Упражнение 23.1. Напишите функцию, которая возвращает дату ивремя спустя ровно месяц от текущей даты (с применением обычногокодированиядатыкакчисла).

Page 284: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнение 23.2. Напишите функцию, которая получает дату ивремяввидечислаивозвращаетчислосекунд,прошедшихсначаладнятойдаты.

Упражнение 23.3. Можете ли вы использовать os.execute, чтобыизменитьтекущуюдиректориювашейпрограммыLua?Почему?

Page 285: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА24

Отладочнаябиблиотека

Отладочнаябиблиотекане является отладчиком,но онапредложитвсе необходимые примитивы для написания вашего собственногоотладчика для Lua. Из соображения быстродействия официальныминтерфейсом к этим примитивам является C API. Отладочнаябиблиотека в Lua— это способ получить к ним доступ напрямую изкодаLua.

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

Отладочная библиотека состоит из двух видов функций:интроспективные функции и ловушки. Интроспективные функции(introspective function) позволяют изучать различные сторонывыполняемойпрограммы,такиекакстекееактивныхфункций,текущаявыполняемаястрока,значенияиименалокальныхпеременных.Ловушки(hook)позволяютнамотслеживатьвыполнениепрограммы.

Важным понятием в отладочной библиотеке является стековыйуровень.Стековыйуровень(stacklevel)—эточисло,котороеотноситсяк конкретной функции, активной в данный момент: у функции,вызвавшей отладочную библиотеку, уровень 1, у функции, котораявызвалаэтуфункцию,уровень2ит.д.

24.1.Интроспективныесредства

Главной интроспективной функцией в отладочной библиотекеявляется debug.getinfo. Ее первый параметр может быть функцией илистековымуровнем.Привызовеdebug.getinfo(foo)длякакой-тофункцииfоовыполучитетаблицуснекоторымиданнымиобэтойфункции.Этатаблицаможетиметьследующиеполя:

Page 286: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

source:гдебылаопределенафункция.Еслиэтафункциябылаопределенавстроке(посредствомloadstring),тозначениемsourceбудетэтастрока.Еслифункциябылаопределенавфайле,тозначениеsource—имяэтогофайласпрефиксом'@';short_src:короткаяверсияsource(до60символов),полезнадлясообщенийобошибках;linedefined:номерпервойстрокивsource,гдефункциябылаопределена;lastlinedefined:номерпоследнейстрокивsource,гдефункциябылаопределена;what:чтоэтозафункция.Возможныезначения:"Lua",еслиэтообычнаяфункцияLua,"С",еслиэтофункцияС,или"main",еслиэтоглавнаячастькускаLua;name:подходящеедляфункцииимя;namewhat:чтоозначаетпредыдущееполе.Возможныезначения:"global","local","method","field"и""(пустаястрока).Пустаястрокаозначает,чтоLuaненашелименидляфункции;nups:количествоверхнихзначенийдляэтойфункции;activelines:таблица,представляющаямножествоактивныхстрокфункции.Активнаястрока(activeline)—этострокаскаким-токодом,вотличиеотпустыхстрокистрок,состоящихтолькоизкомментариев.(Типичноеиспользованиеданнойинформации—этоустановкаточекпрерывания(breakpoint).Большинствоотладчиковнепозволяетзадаватьточкипрерыванияненаактивныхстроках,таккаконибылибынедостижимы.)func:самафункция;обэтомпозже.

Когда foo является функцией С, у Lua о ней почти нет никакихданных.Длятакихфункцийзначимылишьполяwhat,nameиnamewhat.

Когда вы вызываете debug.getinfo(n) для какого-то числа n, выполучаетеданныеофункции,активнойнаэтомуровнестека.Например,еслиnравно1,товыполучаетеданныеофункции,совершающейвызов.(Когда n равно 0, вы получаете данные о самой функции getinfo, т.е.функцию C.) Если n больше числа активных функций в стеке, тоdebug.getinfoвозвращаетnil.Когдавыопрашиваетеактивнуюфункцию,вызывая debug.getinfo с числовым аргументом, у итоговой таблицыбудет одно дополнительное поле с именем сurrentline, содержащееномер строки, на которойнаходитсяфункция в данныймомент.Крометого,funcсодержитфункцию,котораяактивнанаэтомуровне.

Page 287: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Полеnameнепростое.Каквыпомните,из-затого,чтофункциивLuaявляются значениямипервого класса,функцияможет вообщене иметьимени или иметь несколько имен. Lua пытается найти имя функциипутемпросмотракода,вызвавшемэтуфункцию,чтобыувидеть,каконее вызвал. Этот метод работает лишь при вызове getinfo с числовымаргументом,тоестькогдамызапрашиваеминформациюоконкретномвызове.

Функция getinfo обладает низкой производительностью. Luaсодержит отладочную информацию в форме, которая не ухудшаетбыстродействие программы; эффективный поиск информации здесьвторичен. Чтобы получить большее быстродействие, у getinfo естьнеобязательный второй параметр, который ограничивает круг поисканеобходимой информации. Таким образом, данная функция не тратитлишнеевремянасборлишнейинформации.Форматданногопараметраявляется строкой, где каждая буква служит для выбора группы полейсогласноследующейтаблице:'n'name,namewhat'f'func'S'source,short_src,what,linedefined,lastlinedefined'l'currentline'L'activelines'u'nup

Следующаяфункцияиллюстрируетиспользованиеdebug.getinfo.Онараспечатываетпримитивнуюобратнуютрассировкуактивногостека:

functiontraceback()

forlevel=1,math.hugedo

localinfo=debug.getinfo(level,"Sl")

ifnotinfothenbreakend

ifinfo.what=="C"then--функцияC?

print(level,"Cfunction")

else--функцияLua

print(string.format("[%s]:%d",info.short_src,

info.currentline))

end

end

end

Эту функцию легко можно улучшить, добавив больше данных изgetinfo. В действительности в отладочной библиотеке уже есть ееулучшеннаяверсия—функцияtraceback.Вотличиеотнашейфункции,debug.tracebackнепечатаетсвойрезультат;вместоэтогоонавозвращает

Page 288: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

(обычнодлинную)строкусобратнойтрассировкой.

Доступклокальнымпеременным

Мыможемизучатьлокальныепеременныелюбойактивнойфункциипри помощи функции debug.getlocal. У этой функции два параметра:стековый уровень опрашиваемой функции и индекс переменной. Онавозвращает два значения: имя переменной и ее текущее значение. Еслииндекс переменной больше числа активных переменных, то getlocalвозвращаетnil.Еслиуказаннедопустимыйстековыйуровень,тоgetlocalвызываетошибку.(Дляпроверкидопустимостиуровнястекамыможемвоспользоватьсяdebug.getinfо.)

Luaнумеруетлокальныепеременныевпорядкеихпоявлениявнутрифункции, считая лишь переменные, которые являются активными втекущей области видимости функции. Например, рассмотримследующуюфункцию:

functionfoo(a,b)

localx

dolocalc=a-bend

locala=1

whiletruedo

localname,value=debug.getlocal(1,a)

ifnotnamethenbreakend

print(name,value)

a=a+1

end

end

Вызовfoo(10,20)напечатаетследующее:a10

b20

xnil

a4

Переменная с индексом1—этоа (первыйпараметр), с индексом2—этоb,3—этох,4—этодругаяа.Вмоментвызоваgetlocalпеременнаясужевышлаизобластивидимости,втовремякакnameиvalueещевнеене вошли. (Вспомните, что локальная переменная видна лишь послеинициализирующегоеекода.)

НачинаясLua5.2,отрицательныеиндексывозвращаютинформациюо дополнительных аргументах функции: индекс -1 соответствуетпервомудополнительномуаргументу.Вэтомслучаеименемпеременной

Page 289: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

всегдабудет"(*vararg)".Вы также можете изменять значения локальных переменных при

помощи функции debug.setlocal. Ее первые два параметра — этоуровень в стеке и индекс переменной, как и в getlocal. Ее третийпараметр— это новое значение для переменной. Функция возвращаетимя переменной или nil, если индекс переменной вне областивидимости.

Доступкнелокальнымпеременным

Отладочнаябиблиотекатакжепозволяетобращатьсякнелокальнымпеременным, используемым функцией Lua, при помощи getupvalue. Вотличие от локальных переменных, нелокальные переменные,используемыефункцией,существуют,дажекогдафункциянеактивна(вконце концов, в этом суть замыканий). Поэтому первый аргумент дляgetupvalue — это не уровень в стеке, а функция (точнее, замыкание).Второйаргумент—этоиндекспеременной.Luaнумеруетнелокальныепеременные в том порядке, в котором они впервые встречаются вфункции, но этот порядок не важен, поскольку функция не можетобратиться сразу к двум нелокальным переменным с одним и тем жеименем.

Вы также можете обновлять нелокальные переменные при помощиdebug.setupvalue. У нее, как вы уже догадались, три параметра:замыкание, имя переменной и новое значение. Как и setlocal, онавозвращает имя переменной или nil, если индекс переменной внедопустимогодиапазона.

Листинг24.1показывает,какмыможемполучитьдоступкзначениюлюбой из этих переменных по ее имени.Параметр level сообщает, гдеименно эта функция должна искать; увеличение на единицу нужно,чтобы не включать вызов к самой функции getvarvalue. Функцияgetvarvalueсначалапроверяетлокальнуюпеременную.Еслипеременныхс заданным именем несколько, то она использует переменную снаибольшиминдексом;такимобразом,онавсегдадолжнапройтивесьцикл. Если функция не может найти ни одной переменной с такимименем, то она проверяет нелокальные переменные. Для этого припомощи debug.getinfo она получает вызывающее замыкание, а затемперебираетвсеегонелокальныепеременные.Наконец,еслифункциинеудается найти нелокальную переменную с заданным именем, то она

Page 290: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

Листинг24.1.Получениезначенияпеременнойfunctiongetvarvalue(name,level)

localvalue

localfound=false

level=(levelor1)+1

--пробуетлокальныепеременные

fori=1,math.hugedo

localn,v=debug.getlocal(level,i)

ifnotnthenbreakend

ifn==namethen

value=v

found=true

end

end

iffoundthenreturnvalueend

--пробуетнелокальныепеременные

localfunc=debug.getinfo(level,"f").func

fori=1,math.hugedo

localn,v=debug.getupvalue(func,i)

ifnotnthenbreakend

ifn==namethenreturnvend

end

--ненайдено;получаетзначениеизокружения

localenv=getvarvalue("_ENV",level)

returnenv[name]

end

Доступкдругимсопрограммам

Все интроспективные функции из отладочной библиотеки могутприниматьвкачествепервогоаргументасопрограмму,чтобымымоглиизучитьсопрограммуизвне.Давайтерассмотримследующийпример:

co=coroutine.create(function()

localx=10

coroutine.yield()

error("someerror")

end)

coroutine.resume(co)

Page 291: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(debug.traceback(co))

Вызовtracebackобработаетсопрограммуco,ирезультатбудетпримернотаким:

stacktraceback:

[C]:infunction'yield'

temp:3:infunction<temp:1>

Данная трассировка не затрагивает вызов resume, поскольку этасопрограммаиглавнаяпрограммавыполняютсявразныхстеках.

Когдасопрограммавызываетошибку,онанераскручиваетстек.Этозначит,чтопослеошибкимыможемегоизучить.Впродолжениенашегопримера, если мы вновь возобновим сопрограмму, это приведет кошибке:

print(coroutine.resume(co))-->falsetemp:4:someerror

Теперь если мы распечатаем трассировку стека, то получим что-товроде:

stacktraceback:

[C]:infunction'error'

temp:4:infunction<temp:1>

При этом мы можем изучать локальные переменные из сопрограммыдажепослеошибки:

print(debug.getlocal(co,1,1))-->x10

24.2.Ловушки

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

call(событиевызова)происходиткаждыйраз,когдаLuaвызываетфункцию;return(событиевозврата)происходиткаждыйразпривозвратеизфункции;line(событиестроки)происходит,когдаLuaначинаетвыполнениеследующейстрокикода;count(событиесчетчика)происходитпослезаданногоколичества

Page 292: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

команд.

Lua вызывает ловушки с единственным аргументом — строкой,описывающей событие, которое привело в вызову: "call" (или "tailcall"),"return","line"или"count".Длясобытиястрокитакжепередаетсявторой аргумент — новый номер строки. Для получениядополнительной информации внутри ловушки следует использоватьdebug.getinfо.

Чтобы зарегистрировать ловушку, мы вызываем функциюdebug.sethook с двумя или тремя аргументами: первый аргумент— этофункцияловушки;второйаргумент—этофильтрующаястрока,котораяописывает, какие именно события мы хотим отслеживать; инеобязательный третий аргумент — это число, задающее с какойчастотой мы хотим получать события счетчика. Чтобы отслеживатьсобытиявызова,возвратаистроки,мыдобавляемихпервыебуквы('c','r'или 'l')кфильтрующейстроке.Дляотслеживаниясобытийсчетчикамыпростопередаемсчетчиккактретийаргумент.Дляотключениявсехловушекнужновызватьsethookбезаргументов.

В качестве простого примера следующий код устанавливаетпримитивный трассировщик, который печатает каждую строку кода,выполняемуюинтерпретатором:

debug.sethook(print,"l")

Этот вызов устанавливает print как функцию ловушки и приказываетLua вызывать ее только при событиях строки. Более проработанныйтрассировщик может использовать getinfo, чтобы добавить ктрассировкеимятекущегофайла:

functiontrace(event,line)

locals=debug.getinfo(2).short_src

print(s..":"..line)

end

debug.sethook(trace,"l")

Для ловушек удобна функция debug.debug. Эта простая функцияпечатает приглашение ввода, которое выполняет любые команды Lua.Онапримерноэквивалентнаследующемукоду:

functiondebug1()

whiletruedo

io.write("debug>")

Page 293: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

localline=io.read()

ifline=="cont"thenbreakend

assert(load(line))()

end

end

Когда пользователь вводит «команду» cont, эта функция завершается.Стандартная реализация очень проста и выполняет команды вглобальном окружении, вне области видимости отлаживаемого кода.Упражнение24.5обсуждаетболееудачнуюреализацию.

24.3.Профилирование

Несмотрянасвоеимя,отладочнаябиблиотекагодитсяидлядругихзадач. Часто такой задачей является профилирование. ДляпрофилированиясучетомвременилучшеиспользоватьинтерфейсС,таккак затраты Lua на вызов каждой ловушки довольно велики и могутсвести на нет любые замеры. Тем не менее, для профилирования наосновеподсчетавызововкодLuaвполнеподходит.Вэтомразделемыразработаем элементарный профилировщик, который подсчитывает,сколькоразбылавызванафункциявовремявыполненияпрограммы.

Главнымиструктурамиданныхнашейпрограммыбудутдветаблицы:одна связывает функции с их счетчиками вызовов, а другая связываетфункции с их именами. Индексами в обеих таблицах будут самифункции.

localCounters={}

localNames={}

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

Теперь мы определим функцию ловушки. Ее задачей являетсяполучить вызваннуюфункциюиувеличить соответствующийсчетчик;приэтомонасобираетименафункций:

localfunctionhook()

localf=debug.getinfo(2,"f").func

localcount=Counters[f]

ifcount==nilthen--firsttime'f'iscalled?

Counters[f]=1

Page 294: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Names[f]=debug.getinfo(2,"Sn")

else--onlyincrementthecounter

Counters[f]=count+1

end

end

Следующим шагом является запуск программы с этой ловушкой. Мыбудемсчитать,чтоглавныйкусокпрограммынаходитсявфайле,ичтопользовательвкачествеаргументапередаетпрофилировщикуимяэтогофайла,напримертак:

%luaprofilermain-prog

С этой схемой профилировщик может взять имя файла из arg[1],включитьловушкуивыполнитьфайл:

localf=assert(loadfile(arg[1]))

debug.sethook(hook,"c")--включаетловушкудлявызовов

f()--выполняетглавнуюпрограмму

debug.sethook()--выключаетэтуловушку

Последнийшаг— это показ результатов.Функция getname из листинга24.2выдаетдлякаждойфункцииееимя.Из-затого,чтоименафункцийв Lua несколько непостоянны, мы добавим к каждой функции ееместоположение, заданное в виде пары файл:номер_строки. Если уфункциинетимени,томыпечатаемлишьееместоположение.Еслиэтофункция С, то мы используем только ее имя (так как у нее нетместоположения). После данного определения мы печатаем каждуюфункциюсеесчетчиком:

forfunc,countinpairs(Counters)do

print(getname(func),count)

end

Листинг24.2.Получениеименифункцииfunctiongetname(func)

localn=Names[func]

ifn.what=="C"then

returnn.name

end

locallc=string.format("[%s]:%d",n.short_src,n.linedefined)

ifn.what~="main"andn.namewhat~=""then

returnstring.format("%s(%s)",lc,n.name)

else

returnlc

end

end

Page 295: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ЕслимыприменимнашпрофилировщиккпримерусцепьюМаркова,который мы разработали в разделе 10.3, то получим результат вродеэтого:

[markov.lua]:4884723

write10000

[markov.lua]:01

read31103

sub884722

[markov.lua]:1(allwords)1

[markov.lua]:20(prefix)894723

find915824

[markov.lua]:26(insert)884723

random10000

sethook1

insert884723

Данный результат указывает на то, что анонимная функция в строке 4(которая является нашим итератором, определенным внутри allwords)былавызвана884723раз,функцияwrite(io.write)былавызвана10000разит.д.

Есть несколько улучшений, которые могут быть внесены в этотпрофилировщик,например,сортировкавывода,улучшеннаяпечатьименфункцийиболеекрасивыйформатвывода.Темнеменее,этотбазовыйпрофилировщикужеитакполезениможетбытьиспользованкакосноваболеепродвинутыхинструментов.

Упражнения

Упражнение 24.1. Почему рекурсия в функции getvarvalue (листинг24.1)обязательноостановится?

Упражнение24.2.Приспособьтефункциюgetvarvalue (листинг24.1)дляработысразличнымисопрограммами(подобнодругимфункциямизотладочнойбиблиотеки).

Упражнение 24.3. Напишите функцию setvarvalue, похожую наgetvarvalue(листинг24.1).

Упражнение24.4.Напишитемодификациюgetvarvalue(листинг24.1)под именем getallvars, которая возвращает таблицу со всемипеременными,которыевидныввызывающейфункции. (Ввозвращаемаятаблица не должна включать в себя переменные окружения; вместоэтогоонадолжнанаследоватьихизисходногоокружения).

Упражнение 24.5. Напишите улучшенную версию debug.debug,

Page 296: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

котораявыполняетзаданныекоманды,какеслибыонибыливыполненыв области видимости вызывающей функции. (Подсказка: выполняйтекоманды в пустом окружении и используйте метаметод __index сприкрепленной функцией getvarvalue для всех обращений кпеременным.)

Упражнение 24.6. Улучшите предыдущий пример, добавивобновлениепеременных.

Упражнение 24.7. Реализуйте некоторые из предложенныхулучшенийдлябазовогопрофилировщикаизраздела24.3.

Упражнение 24.8. Напишите библиотеку для точек прерывания(breakpoint).Онадолжнапредлагатькакминимумдвефункции:

setbreakpoint(function,line)-->возвращаетhandle

removebreakpoint(handle)

Точка прерывания задается функцией и строкой внутри этойфункции. Когда программа сталкивается с точкой прерывания, этабиблиотека должна вызывать debug.debug. (Подсказка: для базовойреализации используйте ловушку строки, которая проверяет, есть ли вней точка прерывания; для улучшения быстродействия приотслеживании выполнения программы используйте ловушку вызова ивключайте ловушку строки лишь тогда, когда программа выполняетзаданнуюфункцию.)

Page 297: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ЧастьIV

CAPI

Page 298: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА25

ОбзорСAPI

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

Вероятно, вы задаетесь вопросом: если Lua — это не автономнаяпрограмма, то как получилось, что мы до сих пор использовали ееотдельно на протяжении всей книги?Ответом на эту загадку являетсяинтерпретаторLua(выполнимыйфайлlua).Этокрошечноеприложение(менеечемизпятисотстроккода),котороеиспользуетбиблиотекуLuaдля реализации автономного интерпретатора. Автомноныйинтерпретаторобеспечиваетвзаимодействиеспользователем,принимаяфайлыистрокидляпоследующейпередачиихбиблиотекеLua,котораяиделаетвсюосновнуюработу(такуюкакнастоящеевыполнениекоданаLua).

Возможностьиспользованиявкачествебиблиотекидлярасширенияприложения— это то, что делает Lua расширяющим языком. В то жевремяпрограмма,котораяиспользуетLua,можетрегистрироватьновыефункции в его окружении; такиефункции реализуютнаC (или другомязыке),темсамымдобавляясредства,которыенемогутбытьнаписанынепосредственнонаLua.Этото,чтоделаетLuaрасширяемымязыком.

Эти два взгляда на Lua (как на расширяющий язык и как нарасширяемыйязык)соответствуютдвумвидамвзаимодействиямеждуСиLua.ВпервомслучаеуправлениеуС,aLua—библиотека.КодСприданномвидевзаимодействиямыназываемприкладнымкодом.Вовторомслучае управление у Lua, а С — библиотека. Здесь код С называетсябиблиотечным кодом. И прикладной, и библиотечный код используетодинитотжеAPIдлявзаимодействиясLua—такназываемыйСAPI.

С API — это набор функций, которые позволяют коду Свзаимодействовать с Lua. (Примечание: Далее в этом тексте термин«функция»насамомделеозначает«функцияилимакрос».APIреализуетнекоторыесредстваввидемакросов.)Онвключаетвсебяфункциидлячтенияи записи глобальныхпеременныхLua, для вызовафункцийLua,для выполнения фрагментов кода на Lua, для регистрации функций С,чтобыпозжеихможнобыловызватьизкодаLuaит.д.Практическивсе,

Page 299: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

что код Lua может сделать, может быть также сделано и на СпосредствомСAPI.

СAPIследуетпринципуработысС,которыйзаметноотличаетсяотпринципа работы с Lua. При программировании на С мы должнезаботиться о проверке типов, о восстановлении после ошибок, обошибках выделения памяти и о некоторых источниках трудностей.Большинство функций API не проверяет правильность своихаргументов; это ваша задача — убедиться в том, что аргументыкорректны перед вызовом функции. (Примечание: Вы можетекомпилировать код с макросом LUA_USE_APICHECK, который задействуетнекоторые проверки; эта опция особенно полезна при отладке вашегокода С. Тем не менее, некоторые ошибки, вроде недопустимыхуказателей,вCпростонемогутбытьобнаружены.)Есливыдопускаетеошибки,томожетеполучитьтакиенеособоинформативныесообщенияоб ошибках, как «segmentation fault». Более того,СAPI делает упор нагибкость и простоту, зачастую за счет легкости использования.Типичные задачи могут потребовать нескольких вызовов API. Этоможет быть утомительным, но зато дает вам полный контроль надпроисходящим.

Целью данной главы, как ясно из ее названия, является обзор того,что вам потребуется при при использовании Lua из С. Не пытайтесьсейчаспонятьвседеталипроисходящего.Мыостановимсянанихпозже.Однако, не забывайте, что вы всегда можете найти более подробнуюинформацию о специфическихфункциях в справочном руководстве поLua. Более того, вы можете найти некоторые примеры использованияAPI в самой поставке Lua. Автономный интерпретатор Lua (lua.с)предоставляетпримерыприкладногокода,втовремякакстандартныебиблиотеки (lmathlib.c, lstrlib.c и т.д.) снабжены примерамибиблиотечногокода.

СэтогомоментавывыступаетевролипрограммистовнаС.Когдаяговорю «вы», то имею в виду вас, как программирующего на С илипытающегосябытьпрограммистомнаC.

Важным компонентом во взаимодействии между Lua и С являетсявездесущий виртуальный стек. Почти все функции API работают созначениями в этом стеке. Весь обмен данными между Lua и Спроисходитчерезэтотстек.Болеетого,вытакжеможетеиспользоватьегодляхраненияпромежуточныхрезультатов.Онпомогаетразобратьсяс двумя принципиальными отличиями между Lua и С: первое отличиезаключается в том, что в Lua есть сборка мусора, в то время как С

Page 300: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

требуетсяявноевысвобождениепамяти;второеотличиевозникаетиз-заогромной пропасти между динамической типизацией в Lua истатическойтипизациейвС.Мыобсудимстекболееподробновразделе25.2.

25.1.Первыйпример

Мыначнемэтотобзорспростогопримераприкладногоприложения:автономного интерпретатора Lua. Мы можем написать примитивныйавтономныйинтерпретаторLua,каквлистинге25.1.Заголовочныйфайлlua.hопределяетосновныефункции,предоставляемыеLua.ОнвключаетвсебяфункциидлясозданияновогоокруженияLua,длявызовафункцийLua(такихкакlua_pcall),длячтенияизаписиглобальныхпеременныхвокруженииLua,длярегистрацииновыхфункцийдлявызоваизLuaит.д.Все,чтоопределеновфайлеlua.h,имеетпрефиксlua_.

Листинг25.1.ПростойавтономныйинтерпретаторLua#include<stdio.h>

#include<string.h>

#include"lua.h"

#include"lauxlib.h"

#include"lualib.h"

intmain(void){

charbuff[256];

interror;

lua_State*L=luaL_newstate();/*открываетLua

*/

luaL_openlibs(L);/*открываетстандартныебиблиотеки

*/

while(fgets(buff,sizeof(buff),stdin)!=NULL){

error=luaL_loadstring(L,buff)||lua_pcall(L,0,0,0);

if(error){

fprintf(stderr,"%s\n",lua_tostring(L,-1));

lua_pop(L,1);/*выталкиваетсообщениеобошибкеизстека

*/

}

}

lua_close(L);

return0;

}

Заголовочныйфайлlauxlib.hопределяетфункции,предоставленные

Page 301: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

вспомогательнойбиблиотекой(auxlib).ВсеихопределенияначинаютсясluaL_ (например, luaL_loadstring). Вспомогательная библиотекаиспользует базовый API, предоставляемый lua.h для обеспеченияабстракций более высокого уровня, в частности абстракций,используемых стандартными библиотеками. Базовый API стремится кэкономичности и независимости, в то время как вспомогательнаябиблиотека стремится к практичности для распространенных задач.Разумеется, для вашей программы тоже очень легко можно создаватьдругие абстракции, которые ей понадобятся. Имейте в виду, что увспомогательнойбиблиотекинетдоступаковнутреннимкомпонентамLua. Всю работу она выполняет посредством официального базовогоAPI.Все,чтоможетон,можетивашапрограмма.

Библиотека Lua вообще не определяет никаких глобальныхпеременных.Онахранитвсе своесостояниевдинамическойструктуреlua_State;всефункциивнутриLuaполучаютуказательнаэтуструктурув качестве аргумента. Эта реализация делает Lua реентерабельным (свозможностью повторного входа в приложение) и готовым киспользованиювмногонитевыхприложениях.

Как следует из ее имени, функция luaL_newstate создает новоесостояние Lua. Когда luaL_newstate создает новое состояние, то егоокружение не содержит никаких встроенных функций, даже print.Чтобы сохранить размер Lua небольшим, все стандартные библиотекипредставлены как отдельные пакеты, поэтому вы не обязаны ихиспользовать, если они вам не нужны. Заголовочный файл lualib.hопределяет функции для открытия библиотек. Функция luaL_openlibsоткрываетвсестандартныебиблиотеки.

После создания состояния и наполнения его стандартнымибиблиотеками пора приступить к обработке данных, вводимыхпользователем. Для каждой строки, которую вводит пользователь,программасначалавызываетluaL_loadstringдлякомпиляцииэтогокода.Если ошибок нет, то этот вызов возвращает ноль и заталкиваетполучившуюся функцию в стек. (Помните, что мы обсудим этот«волшебный» стек в деталях в следующем разделе.) После этогопрограммавызываетlua_pcall,котораявыталкиваетфункциюизстекаивыполняет ее в защищенном режиме. Как и luaL_loadstring, функцияlua_pcall возвращает ноль, если нет ошибок. В случае ошибки обефункции заталкивают в стек сообщение об ошибке; мы получим этосообщение при помощи lua_tostring, и после того, как мы егонапечатаем,мыудалимегоизстекаприпомощиlua_рор.

Page 302: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Обратитевнимание,чтовслучаеошибкипрограммапростопечатаетсообщение об ошибке в стандартный поток для ошибок. Настоящаяобработка ошибок в С может быть довольно сложной, и то, как ееследует выполнять, зависит от типа вашего приложения. Ядро Luaникогданичегонепечатаетнивкакойвыходнойпоток;оносообщаетоб ошибках посредством возвращения сообщений о них. Каждоеприложениеможетобрабатыватьэтисообщениянаиболееподходящимдлянего способом.Чтобыданныерассуждения сталипонятнее,мынавремя прибегнем к простому обработчику ошибок, который печатаетсообщениеобошибке,закрываетсостояниеLuaипроизводитвыходизвсегоприложения:

#include<stdarg.h>

#include<stdio.h>

#include<stdlib.h>

voiderror(lua_State*L,constchar*fmt,...){

va_listargp;

va_start(argp,fmt);

vfprintf(stderr,fmt,argp);

va_end(argp);

lua_close(L);

exit(EXIT_FAILURE);

}

Позжемыещевернемсякобработкеошибоквприкладномкоде.ПосколькувыможетекомпилироватьLuaикаккоднаС,икаккодна

C++,lua.hневключаетвсебяэтоттипичныйкоддляпоправок,которыйбываетвнекоторыхдругихбиблиотекахС:

#ifdef__cplusplus

extern"C"{

#endif

...

#ifdef__cplusplus

}

#endif

Если вы компилируете Lua как код С (наиболее частый случай) ииспользуете его в C++, вы можете включать lua.hpp вместо lua.h. Онопределенследующимобразом:

extern"C"{

#include"lua.h"

}

Page 303: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

25.2.Стек

При обмене значениями между Lua и С мы сталкиваемся с двумясложностями: несоответствие между статической и динамическойсистемами типизации и несоответствие между автоматическим иручнымуправлениемпамятью.

ВLua,когдамыпишема[k]=v,переменныеkиvмогутиметьсамыеразные типы, даже а может иметь другой тип из-за примененияметатаблиц.Однако, еслимыхотимпредложить эту операциювС, токаждаяотдельновзятаяфункцияsettableдолжнаиметьфиксированныйтип. Нам понадобятся десятки разных функций для этой простойоперации (по одной функции на каждую комбинацию из типов трехаргументов).

Мыможемрешитьданнуюпроблему,введянечтовродетипаunionизC—назовемегоlua_Value,которыйможетпредставлятьвсезначениявLua.Тогдамымоглибыобъявитьsettableкак

voidlua_settable(lua_Valuea,lua_Valuek,lua_Valuev);

Однако, у этогорешенияестьдванедостатка.Во-первых,можетбытьдовольно сложно адаптировать столь комплексный тип данных поддругие языки; мы разрабатывали Lua так, чтобы он легковзаимодействовал не только с C/C++, но также и с Java, Fortran, C# идругимиязыками.Во-вторых,Luaосуществляетсборкумусора:еслимыхраним таблицу Lua в переменной С, то движок Lua о такомиспользовании никак знать не может; он мог бы (ошибочно)предположить,чтоэтатаблицаявляетсямусором,иудалитьее.

Таким образом, Lua API не определяет типы подобные lua_Value.Вместо этого он использует абстрактный стек для обмена значениямимежду Lua и С. Каждый слот в этом стеке может содержать любоезначение Lua. Каждый раз, когда вам нужно получить значение от Lua(например, значение глобальной переменной), вы вызываете Lua, и онзаталкиваетнужноезначениевстек.КогдавыхотитепередатьзначениевLua, товысперва заталкиваете егов стек,илишь затемвызываетеLua(который вытолкнет это значение из стека). Нам по-прежнему нужнаодна функция для заталкивания каждого типа С в стек и другая дляполучения каждого типа C из стека, но зато мы избежаликомбинаторный взрыв. Более того, поскольку этот стек живет внутриLua,тосборщикмусоразнает,какиезначенияиспользуетС.

Page 304: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ПрактическивсефункциивAPIиспользуютстек.Какмыужевиделив нашем первом примере, luaL_loadstring оставляет свой результат встеке (либо как скомпилированный кусок, либо как сообщение обошибке);lua_pcallполучаетфункцию,которуюнеобходимовызватьизстека,иоставляетвнемсообщениеобошибке,еслионапроизойдет.

Lua работает со стеком строго в соответствии с принципом LIFO(Last In, First Out — последним вошел, первым вышел). Когда вывызываете Lua, то он меняет лишь верхнюю часть стека. У кода Сбольшесвободы;вчастности,онможетпросматриватьлюбойэлементвнутри стека и даже вставлять и удалять элементы на любойпроизвольнойпозиции.

Заталкиваниеэлементов

ВAPIсодержитсяпооднойфункциизаталкиваниядлякаждоготипаС, который может быть представлен в Lua: lua_pushnil для константыnil, lua_pushboolean для логических значений (целые числа в С),lua_pushnumber для чисел с плавающей точкой двойной точности,lua_pushinteger для целых чисел со знаком, lua_pushunsigned для целыхчисел без знака, lua_pushlstring для произвольных строк (указатель наcharплюсдлина)иlua_pushstringдлястрок,которыезавершаютсянуль-символом:

voidlua_pushnil(lua_State*L);

voidlua_pushboolean(lua_State*L,intbool);

voidlua_pushnumber(lua_State*L,lua_Numbern);

voidlua_pushinteger(lua_State*L,lua_Integern);

voidlua_pushunsigned(lua_State*L,lua_Unsignedn);

voidlua_pushlstring(lua_State*L,constchar*s,size_tlen);

voidlua_pushstring(lua_State*L,constchar*s);

Также есть функции для заталкивания в стек функций С и значенийпользовательскихданных;мыобсудимихпозже.

Типlua_Number— это числовой тип в Lua.По умолчанию он равенdouble, но в некоторых дистрибутивах онможет быть заменен на floatили даже на long integer для адаптации под компьютеры с сильноограниченнымиресурсами.Типlua_Integer—этоцелочисленныйтипсознаком, достаточно большой, чтобы хранить в себе размер большихстрок. Обычно он определен как тип ptrdiff_t. Тип lua_Unsigned

(который появился в Lua 5.2) — это 32-битовый беззнаковыйцелочисленный тип в С; он используется библиотекой для побитовых

Page 305: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

операцийисоответственнымифункциями.Строки вLua не завершаются нуль-символом; онимогут содержать

произвольные бинарные данные. Соответственно, их длина должнабыть задана явно. Основной функцией для заталкивания строки в стекявляетсяlua_pushlstring,длякоторойтребуетсяявноуказыватьдлинувкачестве аргумента. Для строк, завершенных нуль-символом, вы такжеможете использовать lua_pushstring, которая для вычисления длиныстрокииспользуетstrlen. Lua никогда не хранит указателина внешниестроки(илиналюбойдругойвнешнийобъект,заисключениемфункцийС, которые всегда статические). Для любой строки, которуюнеобходимо хранить, Lua или делает копию, или повторно используетсуществующую.Соответственно, выможетеосвободитьилиизменитьвашбуфер,кактолькоизэтихфункцийвернетсяуправление.

Когда вы заталкивате элемент в стек, то ваша обязанность —проследить,чтобывстекедлянегобылодостаточноместа.Помнитеотом,чтосейчасвыпрограммистнаС;Luaвасбаловатьнестанет.КогдаLua начинает выполнение и в любой момент, когда Lua вызывает С, встекеестькакминимум20свободныхслотов.(Заголовочныйфайлlua.hопределяет эту константу как LUA_MINSTACK.) Этого места более чемдостаточно для большинства задач, поэтому, как правило, об этомможнодаженедумать.Однако,длянекоторыхзадачтребуетсябольшеместа в стеке, в частности если у вас есть цикл, который заталкиваетэлементы в стек. В этих случаях вы можете вызвать функциюlua_checkstack, которая проверяет, достаточно ли в стеке места длявашихнужд:

intlua_checkstack(lua_State*L,intsz);

Обращениекэлементам

ДляобращениякэлементамвстекеAPIиспользуетиндексы.Первыйпомещенныйвстекэлементимеетиндекс1,следующий—индекс2,итакдосамойвершины.Мытакжеможемобращатьсякэлементамстека,приняв вершину стека за отправную точку и используя отрицательныеиндексы.Вэтомслучае-1соответствуетэлементунавершинестека(тоесть помещенному в стек последним), -2 соответствует предыдущемуэлементуит.д.Например,вызовlua_tostring(L,-1)возвращаетзначениена вершине стека как строку. Как мы увидим, в одних случаях стекудобнее индексировать с его основания (то есть используя

Page 306: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

положительные индексы), а в других более естественно использоватьотрицательныеиндексы.

Для проверки того, является ли элемент значением заданного типа,API предлагает семейство функций lua_is*, где * может быть любымтипом Lua. Соответственно, есть функции lua_isnumber, lua_isstring,lua_istableит.д.Увсехэтихфункцийодинитотжепрототип:

intlua_is*(lua_State*L,intindex);

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

Также существует функция lua_type, которая возвращает типэлементавстеке.Каждыйтиппредставленконстантой,определеннойвзаголовочном файле lua.h: LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER,LUA_TSTRING, LUA_TTABLE, LUA__TTHREAD, LUA_TUSERDATA и LUA_TFUNCTION.Обычно мы используем эту функцию совместно с оператором switch.Также она оказывается полезной, когда нам нужно проверить значениенапринадлежностькчисламилистрокамбезприведениятипов.

Дляполучениязначенийизстекаприменяютсяфункцииlua_to*:intlua_toboolean(lua_State*L,intindex);

constchar*lua_tolstring(lua_State*L,intindex,size_t*len);

lua_Numberlua_tonumber(lua_State*L,intindex);

lua_Integerlua_tointeger(lua_State*L,intindex);

lua_Unsignedlua_tounsigned(lua_State*L,intidx);

Функция lua_toboolean преобразует любое значение Lua в логическоезначениеС(0или1),следуяправиламLuaдлявыраженийусловия:nilиfalseложны,всеостальныезначенияистинны.

Допустимо вызывать любую из lua_to* функций, даже когдазаданный элемент не обладает подходящим типом. Функцияlua_toboolean работает для любого типа; lua_tolstring возвращает NULLдля нестроковых значений. Однако, у числовых функций нетвозможностисообщитьонеправильномтипе,поэтомувслучаеошибкиони просто возвращают ноль.Обычно для проверки типа вам следуетвызыватьlua_isnumber,новLua5.2ввелиследующиеновыефункции:

lua_Numberlua_tonumberx(lua_State*L,intidx,int*isnum);

lua_Integerlua_tointegerx(lua_State*L,intidx,int*isnum);

lua_Unsignedlua_tounsignedx(lua_State*L,intidx,int*isnum);

Page 307: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Выходной параметр isnum возвращает булево значение, сообщающее отом,былолизначениеLuaчислом.(Есливамэтозначениененужно,товы можете в качестве последнего параметра передать NULL. Старыефункции lua_to* теперь реализованы как макросы на основе этихфункций.)

Функция lua_tolstring возвращает указатель на внутреннюю копиюстроки и хранит длину строки в позиции, заданной len. Вы не можетеизменятьэтувнутреннююкопию(выставленныйconstнапомнитвамобэтом).Luaследитзатем,чтобыэтотуказательбылдействителендотехпор,покасоответствующеестроковоезначениенаходитсявстеке.Когдафункция С, вызванная из Lua, возвращает управление, Lua очищает еестек;поэтому,какправило,выникогданедолжныхранитьуказателинастрокиLuaвнефункции,получившейих.

Любаястрока,которуювозвращаетlua_tolstring,всегдасодержитвконце дополнительный ноль, но она может содержать внутри себя идругие ноли. Настоящая длина строки возвращается через третийаргументlen.Вчастности,еслипредположить,чтозначениенавершинестека является строкой, то следующие функции assert всегдадействительны:

size_tl;

constchar*s=lua_tolstring(L,-1,&l);/*любаястрокаLua*/

assert(s[l]=='\0');

assert(strlen(s)<=l);

Вы можете вызвать lua_tolstring с NULL в качестве третьегоаргумента,есливамненужнаэтадлина.Аещелучшевоспользоватьсямакросом lua_tostring, который просто вызывает lua_tolstring стретьимаргументом,равнымNULL.

Чтобы проиллюстрировать применение этих функций листинг 25.2содержит полезную вспомогательную функцию, которая печатает всесодержимое стека. Эта функция обходит весь стек от основания довершины, печатая каждый элемент в соответствии с его типом. Стокипечатаютсявкавычках,длячиселиспользуетсяформат '%g'; длядругихзначений(функции,таблицыит.п.)печатаетсятолькоихтип. (Функцияlua_typenameпреобразуеткодтипавназваниетипа.)

Листинг25.2.ПечатьсодержимогостекаstaticvoidstackDump(lua_State*L){

inti;

inttop=lua_gettop(L);/*глубинастека*/

Page 308: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

for(i=1;i<=top;i++){/*повторяетдлякаждогоуровня*/

intt=lua_type(L,i);

switch(t){

caseLUA_TSTRING:{/*строки*/

printf("'%s'",lua_tostring(L,i));

break;

}

caseLUA_TBOOLEAN:{/*булевызначения*/

printf(lua_toboolean(L,i)?"true":"false");

break;

}

caseLUA_TNUMBER:{/*числа*/

printf("%g",lua_tonumber(L,i));

break;

}

default:{/*другиезначения*/

printf("%s",lua_typename(L,t));

break;

}

}

printf("");/*помещаетразделитель*/

}

printf("\n");/*конецлистинга*/

}

Другиестековыеоперации

Кромепредыдущихфункций,служащихдляобменаданнымимеждуСистеком,данныйAPIтакжепредоставляетследующиеоперациидляобщейработысостеком:

intlua_gettop(lua_State*L);

voidlua_settop(lua_State*L,intindex);

voidlua_pushvalue(lua_State*L,intindex);

voidlua_remove(lua_State*L,intindex);

voidlua_insert(lua_State*L,intindex);

voidlua_replace(lua_State*L,intindex);

voidlua_copy(lua_State*L,intfromidx,inttoidx);

Функцияlua_gettopвозвращаетчислоэлементоввстеке,котороетакжеявляетсяиндексомверхнегоэлемента.Функцияlua_settopустанавливаетвершину (то есть количество элементов в стеке)на заданное значение.Еслипредыдущаявершинабылавышеновой,тофункцияотбрасываетсвершины эти лишние значения. В противном случае она заталкивает встек необходимое количество nil для получения заданного размера. Вчастности,lua_settop(L,0)очищаетвесьстек.Вфункцииlua_settop вы

Page 309: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

такжеможетеиспользовать отрицательныеиндексы.Используя данноесредство,APIпредоставляетследующиймакрос,которыйвыталкиваетизстекаnэлементов:

#definelua_pop(L,n)lua_settop(L,-(n)-1)

Функцияlua_pushvalueзаталкиваетвстеккопиюэлементасзаданныминдексом; lua_remove удаляет элемент с заданным индексом, сдвигаявниз все элементы поверх данной позиции, чтобы заполнить разрыв;lua_insert перемещает элемент с вершины стека в заданную позицию,сдвигая вверх все элементы над данной позицией, чтобы освободитьместо; lua_replace выталкивает значение с вершины стека иустанавливает егокак значение элемента с заданныминдексом,ничегопри этом не перемещая; наконец, lua_copy копирует значение одногоиндекса в значение другого, не изменяя исходное значение. Обратитевнимание,чтоследующиеоперациивлияютлишьнапустойстек:

lua_settop(L,-1);/*устанавливаеттекущеезначениевершиныстека

*/

lua_insert(L,-1);/*помещаетэлементнавершинустека*/

lua_copy(L,x,x);/*копируетэлементнаегособственнуюпозицию*/

Программа в листинге 25.3 использует stackDump (определенную влистинге25.2)дляиллюстрацииэтихоперацийнадстеком.

Листинг25.3.Примероперацийнадстеком#include<stdio.h>

#include"lua.h"

#include"lauxlib.h"

staticvoidstackDump(lua_State*L){

<каквлистинге25.2>}

intmain(void){

lua_State*L=luaL_newstate();

lua_pushboolean(L,1);

lua_pushnumber(L,10);

lua_pushnil(L);

lua_pushstring(L,"hello");

stackDump(L);

/*true10nil'hello'*/

lua_pushvalue(L,-4);stackDump(L);

/*true10nil'hello'true*/

Page 310: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

lua_replace(L,3);stackDump(L);

/*true10true'hello'*/

lua_settop(L,6);stackDump(L);

/*true10true'hello'nilnil*/

lua_remove(L,-3);stackDump(L);

/*true10truenilnil*/

lua_settop(L,-5);stackDump(L);

/*true*/

lua_close(L);

return0;

}

25.3.ОбработкаошибоквCAPI

Все структуры в Lua являются динамическими: они растут по меренеобходимостииуменьшаютсявразмере, когда этоосуществимо.Этоозначает, что в Lua постоянно присутствует возможность сбоя привыделении памяти. С этим может столкнуться практически каждаяоперация.Болеетого,многиеоперациимогутвызватьидругиеошибки;например, обращение к глобальной переменной может привести ксрабатываниюметаметода__index,которыйприэтомможетвыброситьошибку. Наконец, операции, которые выделяют память, со временемприводят к срабатыванию сборщика мусора, который может вызватьфинализаторы,которыетакжемогутвыброситьошибки.Корочеговоря,подавляющее большинство функций в Lua API может привести кошибкам.

Вместоиспользованиякодовошибокдлякаждойоперациив своемAPI, Lua использует исключения для уведомления об ошибках. Вотличие от C++ или Java, язык С не содержит механизм обработкиисключений. Чтобы обойти данное ограничение, Lua используетфункциюsetjmpизС,котораяпозволяетполучитьмеханизм,похожийнаобработку исключений. Поэтому большинство функций API можетвыбросить ошибку (то есть вызвать longjmp) вместо возвратауправления.

Когда мы пишем библиотечный код (то есть функции C, которыебудутвызваныизLua),использованиеlongjmpпочтитакжеудобно,какииспользование настоящих средств обработки исключений, посколькуLua отлавливает любую возникающую ошибку. Когда мы пишемприкладнойкод(тоестькодС,которыйвызываетLua),томыдолжныобеспечитьспособдляперехватаподобныхошибок.

Page 311: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Обработкаошибоквприкладномкоде

Когда ваше приложение вызывает функции из Lua API, оноподвержено ошибкам. Как мы только что обсуждали, Lua обычносообщаетобэтихошибкахпосредствомфункцииlongjmp.Однако,еслинет соответствующего вызова setjmp, то интерпретатор не можетвыполнить и longjmp. В этом случае любая ошибка в API приводит ктому, что Lua вызывает паническую функцию (panic function), и еслиуправление из этой функции возвращается, то происходит выход изприложения.Выможетезадатьсвоюпаническуюфункциюприпомощиlua_atpanic,нотакаяфункциямалочтоможетсделать.

Чтобы правильно обрабатывать ошибки в вашем прикладном коде,выдолжнывызыватьвашкодчерезLua,таккаквэтомслучаеонможетустановить подходящий контекст для перехвата ошибок (то есть онвыполнит ваш код в контексте setjmp). Точно так же, как мы можемзапускатькодLuaвзащищенномрежимеприпомощиpcall,мыможемвыполнятькодСпосредствомlua_pcall.Точнее,мызапаковываемкодСвфункциюивызываемэтуфункциючерезLua,используяlua_pcall.(Мыподробнообсудим,каквызыватьфункцииСизLuaвглаве27.)Стакойнастройкой ваш кодС будет выполнен в защищенном режиме. Даже вслучае ошибки выделения памяти lua_pcall возвращаетсоответствующий код ошибки, оставляя интерпретатор в рабочемсостоянии.

Обработкаошибоквбиблиотечномкоде

Lua—этобезопасныйязык.Этозначит,чтонезависимооттого,чтовыпишитенаLua,инаскольконеправильновыэтопишите, вывсегдаможете понять поведение программы, не выходя за рамки самого Lua.Болеетого,ошибкитожеобнаруживаютсяиобъясняютсяврамкахLua.Для контраста сравните с С, где поведение многих неправильнонаписанных программ может быть объяснено лишь в рамкахиспользуемого оборудования (например,места ошибок вC заданы какадресакоманд).

Когда вы добавляете функцию С к Lua, вы нарушаете этубезопасность. Например, такая функция, как роке, которая записываетпроизвольный байт по произвольному адресу памяти, может привестиковсемвидамповрежденияданныхвпамяти.Выдолжныстремитьсяк

Page 312: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

тому,чтобывашидополнительныекомпонентыбылибезопасныдляLuaиобеспечивалихорошуюобработкуошибок.

Как мы ранее обсуждали, программы С должны задавать своюобработку ошибок посредством lua_pcall. Тем не менее, когда выпишете библиотечные функции для Lua, им обычно не требуетсяобрабатыватьошибки.Ошибки,выброшенныебиблиотечнойфункцией,будут пойманы либо при помощи pcall в Lua, либо при помощиlua_pcall в прикладном коде. Поэтому, когда функция в библиотеке Собнаруживает ошибку, она может просто вызвать lua_error (или, чтоещелучше,—luaL_error,котораяформатируетсообщениеобошибкеизатем вызывает lua_error). Функция lua_error очищает все, что нужноочистить в Lua, и перепрыгивает обратно к защищенному вызову, скоторогоначиналосьтовыполнение,передаваяприэтомсообщениеобошибке.

Упражнения

Упражнение 25.1. Скомпилируйте и запустите простой автономныйинтерпретаторLua(листинг25.1).

Упражнение 25.2. Предположим, что стек пустой. Каким будет егосодержимоепослеследующейпоследовательностивызовов?

lua_pushnumber(L,3.5);

lua_pushstring(L,"hello");

lua_pushnil(L);

lua_pushvalue(L,-2);

lua_remove(L,1);

lua_insert(L,-2);

Упражнение 25.3. Используйте простой автономный интерпретаторLua(листинг25.1)ифункциюstackDump(листинг25.2),чтобыпроверитьвашответкпредыдущемуупражнению.

Page 313: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА26

Расширениевашегоприложения

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

26.1.Основы

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

--задаетразмерокна

width=200

height=300

Теперь вы должны использовать Lua API, чтобы заставить Luaразобратьэтотфайл,изатемполучитьзначенияглобальныхпеременныхwidthиheight.Функцияloadизлистинга26.1выполняетэтуработу.Этафункция предполагает, что вы уже создали состояние Lua, следуяувиденному в предыдущей главе. Она вызывает luaL_loadfile длязагрузки куска из файла fname и затем вызывает lua_pcall для запускаскомпилированногокуска.Вслучаеошибок(например,синтаксическихошибок в вашем конфигурационном файле) эти функции заталкиваютсообщениеобошибкевстекивозвращаютненулевойкодошибки;затемнаша программа использует lua_tostring с индексом -1 для получениясообщениясвершиныстека. (Мыопределилифункциюerror в разделе25.1.)

Page 314: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг26.1.Получениепользовательскойинформацииизконфигурационногофайла

voidload(lua_State*L,constchar*fname,int*w,int*h){

if(luaL_loadfile(L,fname)||lua_pcall(L,0,0,0))

error(L,"cannotrunconfig.file:%s",lua_tostring(L,-1));

lua_getglobal(L,"width");

lua_getglobal(L,"height");

if(!lua_isnumber(L,-2))

error(L,"'width'shouldbeanumber\n");

if(!lua_isnumber(L,-1))

error(L,"'height'shouldbeanumber\n");

*w=lua_tointeger(L,-2);

*h=lua_tointeger(L,-1);

}

Послевыполненияэтогокускапрограмменужнополучитьзначенияглобальныхпеременных.Дляэтогоонадваждывызываетlua_getglobal,чьимединственнымпараметром(кромевездесущегоlua_state)являетсяимя переменной. Каждый такой вызов заталкивает соответствующееглобальноезначениевстек,поэтомуширинаокнабудетпоиндексу-2,авысотапоиндексу-1(навершине).(Посколькустекранеебылпуст,вытакжеможетеиндексироватьсоснованиястека,тоестьиспользовать1дляпервого значенияи2длявторого.Однако,индексируяс вершины,вашему коду не нужно проверять, что стек пуст.) Далее наш примериспользует lua_isnumber, чтобы проверить каждое значение насоответствие числу. Затем она вызывает lua_tointeger дляпреобразования таких значений в целые числа, назначая их на своисоответственныепозиции.

СтоилолииспользоватьLuaдляэтойзадачи?Какясказалранее,длятакой простой задачи простой файл, в котором только два числа,вероятно использовать проще, чем файл Lua. Но даже в этом случаеприменение Lua дает некоторые преимущества. Во-первых, Luaобрабатываетзавасвсесинтаксическиедетали;вашконфигурационныйфайл может даже содержать комментарии! Во-вторых, у пользователяпоявляется возможность выполнить с его помощью сложноеконфигурирование. Например, этот скрипт может запросить упользователя какую-то информацию или взять значение из переменнойокружениядлявыбораподходящегоразмера:

--конфигурационныйфайл

ifgetenv("DISPLAY")==":0.0"then

width=300;height=300

Page 315: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

else

width=200;height=200

end

Даже в таких простых конфигурационных сценариях труднопредвидеть, что может понадобиться пользователям; но пока скриптопределяетэтидвепеременные,вашапрограмманаСбудетработатьбезизменений.

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

26.2.Работастаблицами

Давайте приспособим этот подход: теперь мы также хотимкофигурировать цвет фона для этого окна. Допустим, окончательнаяспефикацияцветасоставленаизтрехчисел,каждоеизкоторыхявляетсяцветовымкомпонентомRGB.ОбычновСэтичислаявляютсяцелымиилежат в некотором диапазоне, например [0, 255]. В Lua, поскольку всечисла являются вещественными, мы можем использовать болееестественныйдиапазон[0,1].

Наивным подходом было бы попросить пользователя задаватькаждыйкомпонентвотдельнойглобальнойпеременной:

--конфигурационныйфайл

width=200

height=300

background_red=0.30

background_green=0.10

background_blue=0

Утакогоподходадванедостатка:онслишкомгромоздкий(настоящимпрограммам могут понадобиться десятки цветов для фона окна,основного текста окна, фона меню и т. п.) и нет способа заранееопределить распространенные цвета, чтобы пользователь мог потомпросто написать background=WHITE. Во избежание этих недостатков, дляпредставленияцветамывоспользуемсятаблицей:

background={r=0.30,g=0.10,b=0}

Применениетаблицпридаетвашемускриптуболеепонятнуюструктуру;теперь пользователю (или приложению) станет легко предопределять

Page 316: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

цветадлядальнейшегоиспользованиявконфигурационномфайле:BLUE={r=0,g=0,b=1.0}

<определениядругихцветов>background=BLUE

Для получения этих значений в С мы можем поступить следующимобразом:

lua_getglobal(L,"background");

if(!lua_istable(L,-1))

error(L,"'background'isnotatable");

red=getcolorfield(L,"r");

green=getcolorfield(L,"g");

blue=getcolorfield(L,"b");

Сначала мы получаем значение глобальной переменной background иубеждаемсявтом,чтоэтотаблица,азатемиспользуемgetcolorfieldдляполучениякаждогокомпонентацвета.

Конечно,функцияgetcolorfield—этонечастьAPI;мыдолжныееопределить.Мывновьсталкиваемсяспроблемойполиморфизма:можетбытьпотенциальномноговерсийфункцийgetcolorfield,отличающихсятипом ключа, типом значения, обработкой ошибок и т.д. Lua APIпредлагает всего одну функцию— lua_gettable, которая работает длявсех типов.Она берет позицию таблицы в стеке, выталкивает ключ изстека и заталкивает в стек соответствующее значение. Наша закрытаяgetcolorfield, определенная в листинге 26.2, считает, что таблицанаходитсяна вершине стека, поэтомупосле заталкиванияключа в стекпосредством lua_pushstring таблица будет находиться по индексу -2.Передвозвратомуправленияфункцияgetcolorfieldвыталкиваетизстекаполученное значение, оставляя стек в томже состоянии, в которомонбылпередэтимвызовом.

Листинг26.2.Отдельнаяреализацияgetcolorfield#defineMAX_COLOR255

/*допустим,чтотаблицанаходитсянавершинестека*/

intgetcolorfield(lua_State*L,constchar*key){

intresult;

lua_pushstring(L,key);/*выталкиваетkey*/

lua_gettable(L,-2);/*получаетbackground[key]*/

if(!lua_isnumber(L,-1))

error(L,"invalidcomponentinbackgroundcolor");

result=(int)(lua_tonumber(L,-1)*MAX_COLOR);

Page 317: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

lua_pop(L,1);/*удаляетчисло*/

returnresult;

}

Посколькуиндексированиетаблицыприпомощистроковогоключаочень распространено, Lua 5.1 ввел специализированную версиюlua_gettableдляэтогослучая:lua_getfield.Используяэтуфункцию,мыможемпереписатьследующиедвестроки:

lua_pushstring(L,key);

lua_gettable(L,-2);/*получаетbackground[key]*/

какlua_getfield(L,-1,key);

(Поскольку мы не заталкиваем строку в стек, индекс таблицы по-прежнемуравен-1вмоментвызоваlua_getfield.)

Мы расширим наш пример еще немного и введем названия цветовдля пользователя. Пользователь по-прежнему сможет использоватьтаблицы цветов, но он также сможет использовать предопределенныеназванияцветовдлянаиболеераспространенныхцветов.Дляреализацииданной возможности нам понадобится таблица цветов в нашейпрограммеС:

structColorTable{

char*name;

unsignedcharred,green,blue;

}colortable[]={

{"WHITE",MAX_COLOR,MAX_COLOR,MAX_COLOR},

{"RED",MAX_COLOR,0,0},

{"GREEN",0,MAX_COLOR,0},

{"BLUE",0,0,MAX_COLOR},

<другиецвета>{NULL,0,0,0}/*граничнаяметка*/

};

Наша реализация создаст глобальные переменные с названиямицветов и проинициализирует эти переменные при помощи цветовыхтаблиц. Результат такой же, как если бы пользователь добавилследующиестрокивсвойскрипт:

WHITE={r=1.0,g=1.0,b=1.0}

RED={r=1.0,g=0,b=0}

<другиецвета>

Page 318: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Для задания полей таблицы мы определим вспомогательнуюфункциюsetcolorfield;оназаталкиваетвстекиндексизначениеполя,азатемвызываетlua_settable:

/*предполагает,чтотаблицанаходитсянавершинестека*/

voidsetcolorfield(lua_State*L,constchar*index,intvalue){

lua_pushstring(L,index);/*key*/

lua_pushnumber(L,(double)value/MAX_COLOR);/*value*/

lua_settable(L,-3);

}

Подобно другим функциям API lua_settable работает для множестваразличных типов, поэтому она берет все свои операнды из стека. Онаберет индекс таблицы в качестве аргумента и выталкивает ключ изначение. Функция setcolorfield предполагает, что перед вызовомтаблица находится на вершине стека (индекс -1); после заталкиванияиндексаизначениявстектаблицабудетнаходитьсяпоиндексу-3.

Lua 5.1 также ввел специализированную версию lua_settable длястроковых ключей под названием lua_setfield. Используя эту новуюфункцию, мы можем переписать наше предыдущее определениеsetcolorfieldследующимобразом:

voidsetcolorfield(lua_State*L,constchar*index,intvalue){

lua_pushnumber(L,(double)value/MAX_COLOR);

lua_setfield(L,-2,index);

}

Следующая функция, setcolor, определяет один цвет. Она создаеттаблицу, устанавливает соответствующие поля и присваивает этутаблицусоответствующейглобальнойпеременной:

voidsetcolor(lua_State*L,structColorTable*ct){

lua_newtable(L);/*создаеттаблицу*/

setcolorfield(L,"r",ct->red);/*table.r=ct->r*/

setcolorfield(L,"g",ct->green);/*table.g=ct->g*/

setcolorfield(L,"b",ct->blue);/*table.b=ct->b*/

lua_setglobal(L,ct->name);/*'name'=table*/

}

Функция lua_newtable создает пустую таблицу и заталкивает ее в стек;вызовыsetcolorfield задаютполяэтойтаблицы;наконец,lua_setglobalвыталкивает таблицу из стека и устанавливает ее как значениеглобальнойпеременнойсзаданнымименем.

Используя вышеуказанные функции, следующий цикл регистрируетвсецветадляконфигурационногоскрипта:

Page 319: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

inti=0;

while(colortable[i].name!=NULL)

setcolor(L,&colortable[i++]);

Помните, что приложение должно выполнить этот цикл до запускаскрипта.

Листинг26.3.Цветакакстрокиилитаблицыlua_getglobal(L,"background");

if(lua_isstring(L,-1)){/*значениеявляетсястрокой?*/

constchar*name=lua_tostring(L,-1);/*получаетстроку*/

inti;/*ищетвтаблицецветов*/

for(i=0;colortable[i].name!=NULL;i++){

if(strcmp(colorname,colortable[i].name)==0)

break;

}

if(colortable[i].name==NULL)/*строканенайдена?*/

error(L,"invalidcolorname(%s)",colorname);

else{/*используетcolortable[i]*/

red=colortable[i].red;

green=colortable[i].green;

blue=colortable[i].blue;

}

}elseif(lua_istable(L,-1)){

red=getcolorfield(L,"r");

green=getcolorfield(L,"g");

blue=getcolorfield(L,"b");

}else

error(L,"invalidvaluefor'background'");

Листинг 26.3 показывает другой вариант реализации именованныхцветов.Вместоглобальныхпеременныхпользовательможетобозначатьимена цветов при помощи строк, записывая настройки в видеbackground="BLUE".Такимобразом,background может быть как таблицей,такистрокой.Приподобномподходеприложениюненужночто-либоделать перед запуском пользовательского скрипта. Вместо этого дляполучения цвета нужно выполнить немного больше работы. Когдаскриптполучаетзначениепеременнойbackground,ондолженпроверить,являетсяли тип этого значения строкой, а затемотыскать эту строкувтаблицецветов.

Какой вариант лучше? В программах С использование строк дляобозначения опций не является хорошей практикой, посколькукомпилятор не может обнаружить опечатки. Однако, в Lua сообщениеоб ошибке в названии цвета вероятно дойдет до того, кто пишет этуконфигурационную «программу». Различие между программистом и

Page 320: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

пользователем несколько размыто; разница между ошибкой во времякомпиляциииошибкойвовремявыполнениянестольвелика.

Сострокамизначениеbackgroundмоглобыбытьстрокойсопечаткой;в этом случае приложение может добавить эту информацию ксообщению об ошибке. Приложение может также сравнивать строкинезависимо от регистра букв, так что пользователь может написать"white", "WHITE" или даже "White". Более того, если пользовательскийскриптнебольшой,ацветовмного,тобылобыстраннорегистрироватьсотни цветов (и создавать сотни таблиц и глобальных переменных),чтобы пользователь выбрал лишь некоторые из них. Со строками выизбежитеданныхиздержек.

26.3.ВызовыфункцийLua

Сильной стороной Lua является то, что конфигурационный файлможет определять функции для вызова приложением. Например, выможете написать приложение для построения графика функции ииспользоватьLua,чтобызадатьэтуфункцию.

ПротоколAPIдлявызовафункцийпрост:во-первых,вызаталкиваетефункциюдлявызова;во-вторых,вызаталкиваетеаргументыдлявызова;затемвыиспользуетеlua_pcallдлядействительноговызовафункции;и,наконец,выполучаетерезультатыизстека.

В качестве примера допустим, что у нашего конфигурационногофайлаестьфункциявродеэтой:

functionf(x,y)

return(x^2*math.sin(y))/(1-x)

end

ВыхотитенаСвычислитьz=f(х,у)длязаданныххиу.Приусловии,что вы уже открыли библиотеку Lua и выполнили конфигурационныйфайл,функцияfвлистинге26.4инкапсулируетэтотвызов.

Листинг26.4.ВызовфункцииLuaизС/*вызываетфункцию'f',определеннуювLua*/

doublef(lua_State*L,doublex,doubley){

intisnum;

doublez;

/*заталкиваетфункциииаргументы*/

lua_getglobal(L,"f");/*функциядлявызова*/

lua_pushnumber(L,x);/*заталкивает1-ыйаргумент*/

Page 321: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

lua_pushnumber(L,y);/*заталкивает2-ойаргумент*/

/*производитвызов(2аргумента,1результат)*/

if(lua_pcall(L,2,1,0)!=LUA_OK)

error(L,"errorrunningfunction'f':%s",

lua_tostring(L,-1));

/*возвращаетрезультат*/

z=lua_tonumberx(L,-1,&isnum);

if(!isnum)

error(L,"function'f'mustreturnanumber");

lua_pop(L,1);/*выталкиваетвозвращенноезначение*/

returnz;

}

Второй и третий аргументы lua_pcall — это число аргументов,которые вы передаете, и число результатов, которые вы хотитеполучить, соответственно. Четвертый аргумент представляет собойфункциюдляобработкиошибок;скоромыэтообсудим.КакивслучаесприсваиваниямивLua,вызовlua_pcallприводитдействительноечисловозвращенных значений к заданному вами числу, при необходимостизаталкивая значения nil или отбрасывая лишние. Перед заталкиваниемрезультатовlua_pcall удаляет из стека функцию и ее аргументы. Когдафункциявозвращаетнесколькозначений,первоезначениезаталкиваетсяпервым;например,есливозвращаютсятризначения,топервоебудетпоиндексу-3,апоследнеепоиндексу-1.

В случае возникновения ошибки во время своего выполненияlua_pcall возвращает код ошибки; кроме того, она заталкиваетсообщениеобошибкевстек (нопо-прежнемувыталкиваетфункциюиее аргументы). Однако, перед заталкиванием сообщения lua_pcall

вызывает функцию обработки сообщения, если она была задана. Длязадания функции обработки сообщения используйте последнийаргумент lua_pcall. Ноль означает, что функции обработки нет, т.е.окончательное сообщение об ошибке является при этом исходным. Впротивном случае этот аргумент должен быть индексом в стеке, покоторому размещена функция обработки сообщения. В таких случаяхобработчикдолженбытьпомещенвстекдовызываемойфункциииееаргументов.

Для нормальных ошибок lua_pcall возвращает код ошибкиLUA_ERRRUN. Два особых вида ошибок заслуживают отдельных кодов,поскольку они никогда не запускают обработчик сообщений. Первыйвид—этоошибкивыделенияпамяти.Дляподобныхошибокlua_pcall

Page 322: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

всегда возвращает LUA_ERRMEM. Второй вид — это ошибки привыполнении самого обработчика сообщений. В этом случае нетникакого смысла заново вызывать обработчик сообщений, поэтомуlua_pcallнемедленновозвращаетуправлениескодомLUA_ERRERR.Lua5.2выделяеттретийвидошибок:когдафинализаторвыбрасываетошибку,lua_pcall возвращает код LUA_ERRGCMM (ошибка в метаметоде сборщикамусора).Этоткодобозначает,чтоошибканесвязананепосредственноссамимвызовом.

26.4.Обобщенныйвызовфункции

В качестве более сложного примера мы построим обертку длявызова функций Lua, используя средства vararg в С. Наша оберточнаяфункция,давайтеназовемееcall_va,принимаетимяфункции,которуюнужно вызвать, строку, описывающую типы аргументов и результатов,затем список аргументов и, наконец, список указателей на переменныедля хранения результатов; она берет на себя все тонкости API. Припомощи этой функции мы можем легко переписать наш предыдущийпримерследующимобразом:

call_va(L,"f","dd>d",x,y,&z);

Строка"dd>d"означает«двааргументатипаdoubleиодинрезультаттипаdouble».Этотдескрипторможетиспользоватьбуквы'd'длятипаdouble,'i'дляцелыхчисели's'длястрок;'>'отделяетаргументыотрезультатов.Еслифункцияничегоневозвращает,то'>'необязателен.

Листинг 26.5 показывает реализациюфункции call_va. Несмотря наее общий вид, она идет тем же путем, что и наш первый пример:заталкиваетфункцию,заталкиваетаргументы(листинг26.6),производитвызов и получает результаты (листинг 26.7). Большая часть кода ненуждаетсявпояснении,но естьнекоторыетонкости.Во-первых, ейненужно проверять, что func является функцией; если это не так, тоlua_pcall вызовет ошибку. Во-вторых, поскольку она заталкиваетпроизвольное число аргументов, она должна проверять наличиесвободного места на стеке. В-третьих, поскольку функция можетвернуть строки, то call_va не может вытолкнуть результаты из стека.Этодолжнаделатьвызывающаяфункцияпослетого,каконапрекратитиспользоватьполучаемыевремяотвременистроковыерезультаты (илипослекопированияихвсоответственныебуферы).

Page 323: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг26.5.Обобщенныйвызовфункции#include<stdarg.h>

voidcall_va(lua_State*L,constchar*func,constchar*sig,...)

{

va_listvl;

intnarg,nres;/*числоаргументовирезультатов*/

va_start(vl,sig);

lua_getglobal(L,func);/*заталкиваетфункцию*/

<аргументыдлязаталкивания(листинг26.6)>

nres=strlen(sig);/*числоожидаемыхрезультатов*/

if(lua_pcall(L,narg,nres,0)!=0)/*dothecall*/

error(L,"errorcalling'%s':%s",func,lua_tostring(L,-1));

<возвращенныерезультаты(листинг26.7)>

va_end(vl);

}

Листинг26.6.Выталкиваниеаргументовдляобобщенноговызовафункции

for(narg=0;*sig;narg++){/*повторяетдлякаждогоаргумента*/

/*проверяетпространствостека*/

luaL_checkstack(L,1,"toomanyarguments");

switch(*sig++){

case'd':/*аргументтипаdouble*/

lua_pushnumber(L,va_arg(vl,double));

break;

case'i':/*аргументтипаint*/

lua_pushinteger(L,va_arg(vl,int));

break;

case's':/*аргументтипаstring*/

lua_pushstring(L,va_arg(vl,char*));

break;

case'>':/*конецаргументов*/

gotoendargs;

Page 324: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

default:

error(L,"invalidoption(%c)",*(sig-1));

}

}

endargs:

Листинг 26.7. Получение результатов для обобщенноговызовафункции

nres=-nres;/*стековыйиндекспервогорезультата*/

while(*sig){/*повторяетдлякаждогорезультата*/

switch(*sig++){

case'd':{/*результаттипаdouble*/

intisnum;

doublen=lua_tonumberx(L,nres,&isnum);

if(!isnum)

error(L,"wrongresulttype");

*va_arg(vl,double*)=n;

break;

}

case'i':{/*результаттипаint*/

intisnum;

intn=lua_tointegerx(L,nres,&isnum);

if(!isnum)

error(L,"wrongresulttype");

*va_arg(vl,int*)=n;

break;

}

case's':{/*результаттипаstring*/

constchar*s=lua_tostring(L,nres);

if(s==NULL)

error(L,"wrongresulttype");

*va_arg(vl,constchar**)=s;

break;

}

default:

error(L,"invalidoption(%c)",*(sig-1));

}

nres++;

}

Упражнения

Page 325: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Упражнение26.1.НапишитепрограммуС,котораячитаетфайлLua,содержащий определение полностью числовой функции f, и строитграфик этой функции. (Вам не нужно делать что-нибудь этакое;программа может строить график, выводя результаты при помощиастериксовASCII('*'),какмыделаливразделе8.1.)

Упражнение 26.2. Измените функцию call_va (листинг 26.5) дляобработкибулевыхзначений.

Упражнение 26.3. Допустим, есть программа, которой необходимоследить за несколькими погодными станциями. Внутри, дляпредставления каждой станции, она использует 4-байтовую строку, иесть конфигурационный файл, который отображает каждую такуюстроку вURL соответствующей станции. Конфигурационныйфайл Luaмогбывыполнятьэтоотображениенесколькимиспособами:

наборглобальныхпеременных,пооднойдлякаждойстанции;однатаблица,отображающаястроковыекодывURL'ы;однафункция,отображающаястроковыекодывURL'ы.

Обсудите плюсы и минусы каждого способа, принимая во вниманиеобщеечислостанций,закономерностиURL'ов(т.е.можетсуществоватьправилопостроенияURL'овизкодов),типыпользователейит.д.

Page 326: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА27

ВызываемСизLua

Когда мы говорим, что Lua может вызывать функции С, это незначит, что Lua может вызвать любую функцию С. (Примечание: Естьпакеты,которыепозволяютLuaвызыватьлюбуюфункциюC,ноонинепереносимыинебезопасны.)Какмывиделивпредыдущейглаве,когдаСвызываетфункциюLua,ейнеобходимоследоватьпростомупротоколудля передачи аргументов и получения результатов. Аналогично, привызове функции C из Lua она должна следовать похожему протоколу.Более того, чтобы Lua мог вызвать функцию С, мы должнызарегистрировать эту функцию, то есть должны надлежащим образомпередатьLuaееадрес.

КогдаLuaвызываетфункциюС,Luaиспользуеттужеразновидностьстека,какуюСиспользуетдлявызовафункцийLua.ФункцияСполучаетсвоиаргументыизстекаизаталкиваетсвоирезультатывстек.

Важнымпонятиемздесьявляетсято,чтостекнеявляетсяглобальнойструктурой; у каждой функции есть свой собственный закрытыйлокальный стек. Когда Lua вызывает функцию С, первый аргументвсегда будет находиться по индексу 1 этого локального стека. ДажекогдафункцияСвызываеткодLua,которыйвновьвызываетэтуже(илидругую) функцию C, каждый из этих вызовов видит лишь свойсобственныйзакрытыйстекспервымаргументомпоиндексу1.

27.1.ФункцииС

В качестве первого примера давайте рассмотрим, как реализоватьупрощенную версию функции, которая возвращает синус заданногочисла:

staticintl_sin(lua_State*L){

doubled=lua_tonumber(L,1);/*получаетаргумент*/

lua_pushnumber(L,sin(d));/*заталкиваетрезультат*/

return1;/*числорезультатов*/

}

Любаяфункция,зарегистрированнаявLua,должнаиметьодинитотжепрототип,определенныйвфайлеlua.hкакlua_CFunction:

Page 327: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

typedefint(*lua_CFunction)(lua_State*L);

СточкизренияС,функцияСполучаетвкачествесвоегоединственногоаргументасостояниеLuaивозвращаетцелоечисло,равноеколичествузначений, которое она возвращает в стек. Поэтому функции не нужноочищать стек перед заталкиванием в него своих результатов. Послевозвращения функцией результатов Lua автоматически сохраняет их иочищаетвесьеестек.

Перед тем, как мы сможем использовать эту функцию из Lua, мыдолжны ее зарегистрировать. Мы творим это небольшое волшебствоприпомощиlua_pushcfunction: онаполучаетуказательнафункциюСисоздает значение типа "function", которое представляет эту функциювнутри Lua. После регистрации функция С ведет себя внутри Lua каклюбаядругаяфункция.

Чтобы на скорую руку проверить l_sin, нужно поместить ее коднепосредственно в наш базовый интерпретатор (листинг 25.1) идобавитьследующиестрокипрямопослевызоваluaL_openlibs:

lua_pushcfunction(L,l_sin);

lua_setglobal(L,"mysin");

Первая строка заталкивает в стек значение типа "function", а втораяприсваивает его значение глобальной переменной mysin. После этихизменений выможете использовать эту новуюфункцию mysin в вашихскриптахLua.(ВследующемразделемырассмотримболееподходящиеспособыкомпоновкиновыхфункцийСсLua.)

Вболеепрофессиональнооформленнойфункциивычислениясинусамы должны проверять тип ее аргумента. Здесь нам поможетвспомогательная библиотека. Функция luaL_checknumber проверяет,действительнолизаданныйаргументявляетсячислом:вслучаеошибкиона выбрасывает о ней информативное сообщение; иначе онавозвращаетсамочисло.Изменениявнашейфункцииминимальны:

staticintl_sin(lua_State*L){

doubled=luaL_checknumber(L,1);

lua_pushnumber(L,sin(d));

return1;/*числорезультатов*/

}

С определением выше, если вы вызовите mysin('а'), то получитеследующеесообщение:

badargument#1to'mysin'(numberexpected,gotstring)

Page 328: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Обратите внимание, как luaL_checknumber автоматически заполняетсообщение номером аргумента (#1), именем функции ("mysin"),ожидаемым типом параметра (number) и настоящим типом параметра(string).

В качестве более сложного примера давайте напишем функцию,которая возвращает содержимое заданной директории. Lua непредоставляет эту функцию в своих стандартных библиотеках,посколькувANSIСнетподходящейфункциидляэтойработы.Здесьмыбудем считать, что у нас POSIX-совместимая система. Наша функция(назовемееdirвLuaиl_dirвС)получаетвкачествеаргументастрокуспутем к директориии возвращаетмассив с данными этой директории.Например, вызов dir("/home/lua") может вернуть таблицу{"sre","bin","lib"}.Вслучаеошибкифункциявозвращаетnilвместесострокойссообщениемобошибке.Полныйкодэтойфункцииприведенвлистинге 27.1. Обратите внимание на использование функцииluaL_checkstring из вспомогательной библиотеки, которая являетсяэкивалентомluaL_checknumberдлястрок.

(В экстремальных условиях данная реализация l_dir может вызватьнебольшуюутечкупамяти.ТрифункцииLua,которыеонавызывает,—lua_newtable, lua_pushstring и lua_settable, могут дать сбой из-занехватки памяти. Если какая-либо из этих функций даст сбой, этовызоветошибкуипрерветвыполнениеl_dir,врезультатечегоclosedirне будет вызвана. Как мы обсудили ранее, для большинства программэто не является большой проблемой: если у программы заканчиваетсяпамять,толучшее,чтоможносделать,—этозавершитьеевыполнение.Тем не менее, в главе 30 мы увидим альтернативную реализациюфункции для работы с директориями, в которой данная проблемаустранена.)

Листинг 27.1. Функция для чтения содержимогодиректории

#include<dirent.h>

#include<errno.h>

#include<string.h>

#include"lua.h"

#include"lauxlib.h"

staticintl_dir(lua_State*L){

DIR*dir;

structdirent*entry;

Page 329: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

inti;

constchar*path=luaL_checkstring(L,1);

/*opendirectory*/

dir=opendir(path);

if(dir==NULL){/*erroropeningthedirectory?*/

lua_pushnil(L);/*returnnil...*/

lua_pushstring(L,strerror(errno));/*anderrormessage*/

return2;/*numberofresults*/

}

/*createresulttable*/

lua_newtable(L);

i=1;

while((entry=readdir(dir))!=NULL){

lua_pushnumber(L,i++);/*pushkey*/

lua_pushstring(L,entry->d_name);/*pushvalue*/

lua_settable(L,-3);

}

closedir(dir);

return1;/*tableisalreadyontop*/

}

27.2.Продолжения

Припомощиlua_pcallиlua_callфункцияС,вызваннаяизLua,может,всвоюочередь,вызватьфункциюLua.Такделаютнекоторыефункциииз стандартной библиотеки: table.sort может вызвать функциюупорядочивания; string.gsub может вызвать функцию замены; pcall иxpcall вызываютфункциив защищенномрежиме.Есливспомнить, чтоглавный код Lua был сам, в свою очередь, вызван из С (основнойпрограммы), то мы получаем примерно такую последовательность: С(основная программа) вызывает Lua (скрипт), который вызывает С(библиотека),которыйвызываетLua(обратныйвызов).

Обычно Lua обрабатывает эти последовательности вызовов безпроблем; в конце концов, эта интеграция с C и является «визитнойкарточкой»языка.Темнеменее,естьоднаситуация,вкоторойподобноепереплетениеможетвызватьзатруднения:сопрограммы.

УкаждойсопрограммывLuaестьсвойсобственныйстек,которыйхранитинформациюобожидающихвызовахэтойсопрограммы.Точнее,стек хранит адрес возврата, параметры и локальные переменныекаждого вызова. Для вызовов функций Lua интерпретатор используетподходящуюструктуруданныхдляреализациистека—гибкийстек(softstack). Однако, для вызовов функций С интерпретатор также должен

Page 330: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

использовать стек С. В конце концов, адрес возврата и локальныепеременныефункцииСживутвстекеС.

Уинтерпретаторазапростоможетбытьнесколькогибкихстеков,ноусредывыполненияANSICестьлишьодинвнутреннийстек.Поэтомусопрограммы в Lua не могут приостановить выполнение функции С:есливцепочкевызовов,начинаясresumeизаканчиваясоответственнойyield есть функция С, то Lua не может сохранить состояние этойфункции, чтобы восстановить его при следующем возобновлении.РассмотримследующийпримервLua5.1:

co=coroutine.wrap(function(a)

returnpcall(function(x)

coroutine.yield(x[1])

returnx[3]

end,a)

end)

print(co({10,3,-8,15}))

-->falseattempttoyieldacrossmetamethod/C-callboundary

Функцияpcall—этофункцияС;поэтомуLuaнеможетприостановитьее, поскольку в ANSI С нет способа приостановить функцию С ивозобновитьеепозже.

Lua 5.2 преодолел данные трудности при помощи продолжений(continuation).Lua5.2реализуетуступкууправленияпосредствомlongjmpтемжеобразом,которымонреализуетошибки.Функцияlongjmpпростоотбрасывает всю информацию о функциях С в стеке C, что делаетневозможным возобновление данных функций. Тем не менее, функцияfooнаCможетзадатьпродолжающуюфункциюfoo-c,котораяявляетсядругойфункциейС,чтобывызватьее,когдапонадобитсявозобновитьfoo.Тоестькогдаинтерпретаторобнаружит,чтоондолженвозобновитьfoo, а longjmp выбросила запись о foo из стека С, то вместо этого онвызоветfoo-c.

Чтобы стало понятнее, давайте рассмотрим пример: реализациюpcall.ВLua5.1.уэтойфункциибылследующийкод:

staticintluaB_pcall(lua_State*L){

intstatus;

luaL_checkany(L,1);/*покрайнеймереодинпараметр*/

status=lua_pcall(L,lua_gettop(L)-1,LUA_MULTRET,0);

lua_pushboolean(L,(status==0));/*status*/

lua_insert(L,1);/*statusявляетсяпервымрезультатом*/

returnlua_gettop(L);/*возвращаетstatus+всерезультаты*/

}

Page 331: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Если функция, вызванная посредством lua_pcall, уступилауправление, то возобновить luaB_pcall позже будет невозможно.Поэтомуинтерпретаторвыдавалошибкувсякийраз,когдамыпыталисьуступить управление внутри защищенного вызова. Lua 5.2 реализуетpcall примерно так, как показано в листинге 27.21.(Примечание:Настоящийкоднемногосложнее,чемпоказаноздесь,посколькуунегоестьнекоторыеобщиечастисxpcallипроверканапереполнениестекаперед заталкиванием в него дополнительного результата в формебулевого значения.). Есть три отличия от версии Lua 5.1: во-первых,новая версия заменила вызов lua_pcall на вызов lua_pcallk; во-вторых,она объединила все, что делается после этого вызова в новуювспомогательнуюфункциюfinishpcall; третьеотличие—этофункцияpcallcont, последний аргумент lua_pcallk, которая являетсяпродолжающейфункцией.

Если нет никаких уступок управления, то lua_pcallk работает вточностикакlua_pcall.Если есть какая-нибудьуступкауправления, товсе становится немного иначе. Если функция, вызванная lua_pcall

пытается уступить управление, то Lua 5.2 вызывает ошибку, как и Lua5.1.Однако, когдафункция, вызваннаяlua_pcallk, уступаетуправление,тоникакихошибокнет:LuaвызываетlongjmpивыбрасываетзаписьдляluaB_pcall из стека С, но сохраняет в гибком стеке ссылку напродолжающую функцию pcallcont. Позже, когда интерпретаторобнаруживает,чтоондолженвернутьсяк1иаВ_рса11 (чтоневозможно),онвместоэтоговызываетпродолжающуюфункциюpcallcont.

ВотличиеотluaВ_рса11,продолжающаяфункцияpcallcontнеможетполучить значение, возвращаемое lua_pcallk. Поэтому Luaпредоставляет специальнуюфункциюдля возвращения статуса вызова:lua_getctx. Когда она вызвана из обычной функции Lua (что в нашемслучаенепроисходит),lua_getctxвозвращаетLUA_OK.Когдаонавызванаиз продолжающей функции, она возвращает lua_yield. Продолжающаяфункция также может быть вызвана при некоторых ошибках; в этомслучаеlua_getctxвозвращаеткодошибки,которыйявляетсятемсамымзначением,возвращаемымlua_callk.

Листинг27.2.Реализацияpcallспродолжениямиstaticintfinishpcall(lua_State*L,intstatus){

lua_pushboolean(L,status);/*первыйрезультат(status)*/

lua_insert(L,1);/*помещаетпервыйрезультатвпервыйслот*/

returnlua_gettop(L);

}

Page 332: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

staticintpcallcont(lua_State*L){

intstatus=lua_getctx(L,NULL);

returnfinishpcall(L,(status==LUA_YIELD));

}

staticintluaB_pcall(lua_State*L){

intstatus;

luaL_checkany(L,1);

status=lua_pcallk(L,lua_gettop(L)-2,LUA_MULTRET,0,

0,pcallcont);

returnfinishpcall(L,(status==LUA_OK));

}

Кроместатусавызова,lua_getctxтакжеможетвернутьконтекстнуюинформацию(contextinformation).Пятыйпараметрдляlua_pcallk—этопроизвольное целое число, которое можно получить через второйпараметр lua_getctx, который является указателем на целое число. Этоцелочисленноезначениепозволяетисходнойфункциипередаватькакую-либо произвольную информацию своему продолжению напрямую.Онаможет передавать дополнительную информацию через стек Lua. (Нашпримернезадействуетэтувозможность.)

Система продолжений Lua 5.2 — это гениальный механизм дляподдержкиуступкиуправления,ноэтонепанацея.НекоторымфункциямС может понадобиться передать слишком много контекста своимпродолжениям. Примеры включают в себя table.sort, котораяиспользуетстекСдлярекурсии,иstring.gsub,котораядолжнаследитьзазахватамиибуферомдлясвоихпромежуточныхрезультатов.Хотяихвозможно переписать с поддержкой уступок управления, выигрыш отэтогонестоитдополнительнойсложности.

27.3.МодулиС

Модуль Lua — это кусок кода, который определяет некоторыефункции Lua и хранит их в подходящих местах, обычно как записи втаблице. Модуль С для Lua ведет себя похожим образом. Кромеопределения своих функций С, он также должен определитьспециальную функцию, которая исполняет роль главного куска вбиблиотекеLua.ЭтафункциядолжнарегистрироватьвсефункцииСизмодуляихранитьихвподходящихдляэтогоместах,обычнокакзаписив таблице. Подобно главному кусоку Lua, эта функция также должнаинициализироватьвсе,чтовмодулетребуетинициализации.

Page 333: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Lua получает функции С посредством данного процессарегистрации.КактолькофункцияСпредставленаисохраненавLua,Luaвызываетеечерезпрямуюссылкунаееадрес(которыймыпередаемLua,когдарегистрируемэтуфункцию).Другимисловами,Luaнезависитотимени функции, расположения пакета или правил видимости, чтобывызвать ее, когда она зарегистрирована. Обычно модуль С имеет однуединственнуюоткрытую (extern)функцию, которая являетсяфункциейдля открытия этой библиотеки. Все остальные функции могут бытьзакрытыми—объявленнымивCкакстатические(static)

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

staticintl_dir(lua_State*L){

<какпрежде>}

Далеемыобъявляеммассив со всемифункциямивмодуле вместе сихсоответственными именами. Этот массив содержит элементы типаluaL_Reg, который является структурой из двух полей: имени функции(строка)иуказателянафункцию.

staticconststructluaL_Regmylib[]={

{"dir",l_dir},

{NULL,NULL}/*sentinel*/

};

В нашем примере есть только одна функция (l_dir) для объявления.Последнейпаройвмассивевсегдаявляется{NULL,NULL}дляобозначенияего конца. Наконец, мы объявляем главную функцию, используяluaL_newlib:

intluaopen_mylib(lua_State*L){

luaL_newlib(L,mylib);

return1;

}

Page 334: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Вызов luaL_newlib создает новую таблицу и заполняет ее парами имя-функция из массива mylib. При возвращении luaL_newlib оставляет встеке новую таблицу, в которой она открывала библиотеку. Функцияluaopen_mylibзатемвозвращает1,чтобывернутьэтутаблицувLua.

После завершения библиотеки мы должны скомпоновать ее синтерпретатором.Наиболееудобныйспособдобитьсяэтогосостоитвиспользовании средств динамической компоновки, если вашинтерпретатор Lua поддерживает эти средства. В этом случае выдолжнысоздатьдинамическуюбиблиотекусвашимкодом(mylib.dllвWindows,mylib.sobвLinux)ипоместитьеегде-нибудьвпутиС.Послеэтихшагов выможете загрузить вашубиблиотекунепосредственноизLuaприпомощиrequire:

localmylib=require"mylib"

Этотвызовкомпонуетдинамическуюбиблиотекуmylib сLua, находитфункциюluaopen_mylib, регистрирует ее какфункциюСи вызывает ее,открывая тем самым модуль. (Это поведение объясняет, почемуluaopen_mylibдолжнаиметьтотжесамыйпрототип,чтоилюбаядругаяфункцияС.)

Динамический компоновщик должен знать имя функцииluaopen_mylib, чтобы найти ее. Он всегда будет искать luaopen_ сприсоединеннымименеммодуля.Поэтомуесливашмодульназываетсяmylib,этафункциядолжнаназыватьсяluaopen_mylib.

Если ваш интерпретатор не поддерживает динамическуюкомпоновку, то вам нужно заново скомпилировать Lua с вашей новойбиблиотекой. Кроме этой перекомпиляции вам понадобится какой-нибудьспособсообщитьинтерпретатору,чтоондолженоткрыватьэтубиблиотеку при открытии нового состояния. Простой способ этосделать—добавитьluopen_mylib в списокстандартныхбиблиотекдляоткрытияспомощьюluaL_openlibsвфайлlinit.с.

Упражнения

Упражнение 27.1. Напишите на С функцию summation, котораявычисляет сумму на основе переменного количества числовыхаргументов:

print(summation())-->0

print(summation(2.3,5.4))-->7.7

print(summation(2.3,5.4,-34))-->-26.3

Page 335: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

print(summation(2.3,5.4,{}))

-->stdin:1:badargument#3to'summation'

(numberexpected,gottable)

Упражнение27.2.Реализуйтефункцию,эквивалентнуюtable.packизстандартнойбиблиотеки.

Упражнение 27.3. Напишите функцию, которая получаетпроизвольноечислопараметровивозвращаетихвобратномпорядке:

print(reverse(1,"hello",20))-->20hello1

Упражнение 27.4. Напишите функцию foreach, которая принимаеттаблицу и функцию, а затем вызывает данную функцию для каждойпарыключ-значениевэтойтаблице:

foreach({x=10,y=20},print)

-->x10

-->y20

(Подсказка:проверьтефункциюlua_nextвсправочникепоLua.)Упражнение 27.5. Перепишите функцию foreach из предыдущего

упражнениятак,чтобывызываемаяфункциямоглауступатьуправление.Упражнение 27.6. Создайте модуль С со всеми функциями из

предыдущихупражнений.

Page 336: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА28

ПриемынаписанияфункцийС

ИофициальныйAPI, и вспомогательная библиотека предоставляютнесколькомеханизмовдляпомощивнаписаниифункцийС.Вэтойглавемырассмотриммеханизмыдляобработкимассивов,обработкистрокисохранениязначенийLuaвС.

28.1.Обработкамассивов

В Lua «массив» — это всего лишь название для таблицы,используемойхарактернымобразом.Мыможемобрабатыватьмассивы,используя тежефункции,чтомыиспользовалидляобработки таблиц,то естьlua_settable иlua_gettable.Однако,APIпредоставляетособыефункциидляобработкимассивов.Первойпричинойпользоватьсяэтимидополнительнымифункциямиявляетсяпроизводительность:обращениек массиву посреди внутреннего цикла алгоритма (например, длясортировки) втречается довольно часто, так что любое увеличениебыстродействия этой операции может значительно повлиять набыстродействие алгоритма в целом. Другой причиной являетсяудобство:целочисленныеключидостаточнораспространены,чтобыкакистроковыезаслужитьособоеобращение.

APIпредоставляетдвефункциидляработысмассивами:voidlua_rawgeti(lua_State*L,intindex,intkey);

voidlua_rawseti(lua_State*L,intindex,intkey);

Описаниефункцийlua_rawgetiиlua_rawsetiнесколькосмущает,таккаконо включает два индекса: index указывает, где в стеке находитсятаблица; key указывает, где в этой таблице находится элемент. Вызовlua_rawgeti(L,t,key) эквивалентен следующей последовательности,когда t является положительным числом (иначе вы должныкомпенсироватьпоявлениеновогоэлементавстеке):

lua_pushnumber(L,key);

lua_rawget(L,t);

Вызов lua_rawseti(L,t,key) (опять же для положительного t)

Page 337: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

эквивалентенданнойпоследовательности:lua_pushnumber(L,key);

lua_insert(L,-2);/*помещает'key'подпредыдущимзначением*/

lua_rawset(L,t);

Обратите внимание, что обе эти функции используют операции спрямымдоступом.Онибыстрее,и,крометого,таблицы,используемыекакмассивы,редкоиспользуютметаметоды.

Листинг28.1.ФункцияmарнаСintl_map(lua_State*L){

inti,n;

/*1-ыйаргументдолженбытьтаблицей(t)*/

luaL_checktype(L,1,LUA_TTABLE);

/*2-ойаргументдолженбытьфункцией(f)*/

luaL_checktype(L,2,LUA_TFUNCTION);

n=luaL_len(L,1);/*получаетразмертаблицы*/

for(i=1;i<=n;i++){

lua_pushvalue(L,2);/*pushf*/

lua_rawgeti(L,1,i);/*pusht[i]*/

lua_call(L,1,1);/*callf(t[i])*/

lua_rawseti(L,1,i);/*t[i]=result*/

}

return0;/*безрезультатов*/

}

В качестве конкретного примера использования этих функцийлистинг28.1реализуетфункциюотображения:онаприменяетзаданнуюфункцию ко всем элементам массива, заменяя каждый элементрезультатом вызова. Этот пример также вводит три новые функции:luaL_checktype,luaL_lenиlua_pcall.

ФункцияluaL_checktype(изфайлаlauxlib.h)проверяет,чтозаданныйаргумент имеет заданный тип; в противном случае она выбрасываетошибку.

Элементарнаяфункцияlua_len (не использованная в примере выше)эквивалентна операции '#'. Из-за метаметодов эта операция можетвернуть объект любого вида, а не только числа; поэтому lua_lenвозвращает свой результат в стек. Функция luaL_len (использованная впримере и взятая из вспомогательной библиотеки) вызывает ошибку,

Page 338: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

если длина не является числом, в противном случае она возвращаетдлинукакцелоечислоC.

Функция lua_call выполняет незащищенный вызов. Она похожа наlua_pcall,нопередаетошибкидальше,аневозвращаетихкод.Когдавыпишете главный код приложения, то вы не должны использоватьlua_call,посколькувамтребуетсяловитьлюбыеошибки.Однако,когдавы пишете функции, использовать lua_call — неплохая идея; еслиошибка возникнет, то мы просто оставим ее кому-нибудь, кто о нейпозаботится.

28.2.Обработкастрок

Когда функция С получает строковый аргумент из Lua, тосуществуют только два правила, которые она должна соблюдать: невыталкивать строку из стека во время обращения к ней и никогда немодифицироватьэтустроку.

Ситуация становится сложнее, когда функции С нужно создатьстроку, чтобы вернуть ее Lua. Теперь код С должен беспокоиться овыделении-высвобождениибуфера,переполненияхбуфераит.п.Темнеменее, Lua API предоставляет некоторые функции, чтобы помочь сэтимизадачами.

Стандартный API обеспечивает поддержку двух самых базовыхстроковых операций: извлечение подстроки и конкатенация строк. Приизвлечении подстроки помните, что базовая операция lua_pushlstringполучаетдлинустрокикакдополнительныйаргумент.ПоэтомуесливыхотитепередатьLuaподстрокустрокиs, расположеннуюотпозицииiдопозицииj(включительно),товамвсеголишьнужносделатьэто:

lua_pushlstring(L,s+i,j-i+1);

В качестве примера допустим, что вам нужна функция, котораяразбивает строку по заданному разделителю (одному символу) ивозвращает таблицу с подстроками. Например, вызовsplit("hi:ho:there",":")долженвернутьтаблицу{"hi","ho","there"}.Листинг 28.2 показывает простую реализацию этой функции. Ей ненужны дополнительные буферы и она не накладывает никакихограничений на размер строк, которые она может обрабатывать. ОбовсехбуферахзаботитсяLua.

Листинг28.2.Разбиениестроки

Page 339: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

staticintl_split(lua_State*L){

constchar*s=luaL_checkstring(L,1);/*субъект*/

constchar*sep=luaL_checkstring(L,2);/*разделитель*/

constchar*e;

inti=1;

lua_newtable(L);/*итоговаятаблица*/

/*повторяетдлякаждогоразделителя*/

while((e=strchr(s,*sep))!=NULL){

lua_pushlstring(L,s,e-s);/*заталкиваетподстроку

*/

lua_rawseti(L,-2,i++);/*вставляетеев

таблицу*/

s=e+1;/*пропускает

разделитель*/

}

/*insertlastsubstring*/

lua_pushstring(L,s);

lua_rawseti(L,-2,i);

return1;/*возвращаетэту

таблицу*/

}

ДляконкатенациистрокLuaпредоставляетвсвомAPIхарактернуюфункцию под названием lua_concat. Она эквивалентна операцииконкатенации .. в Lua; она преобразует числа в строки и принеобходимости вызывает метаметоды. Более того, она может за разобъединить более двух строк. Вызов lua_concat(L, n) соединит (ивытолкнет)nзначенийнавершинестекаизаталкнетвнеерезультат.

Другойполезнойфункциейявляетсяlua_pushfstring:constchar*lua_pushfstring(lua_State*L,constchar*fmt,...);

ОнанесколькопохожанафункциюsprintfизCтем,чтосоздаетстрокупо форматирующей строке и некоторым дополнительным аргументам.Однако, в отличие от sprintf, вам не нужно предоставлять буфер. Luaдинамическисоздастстрокудляваснастолькобольшой,насколькоэтонеобходимо. Эта функция заталкивает получившуюся строку в стек ивозвращаетуказательнанее.Вамненужнобеспокоитьсяопереполнениибуфера.На данный момент эта функция поддерживает только директивы длявставки следующих элементов (Примечание: Директива %p дляуказателейпоявилисьвLua5.2.):

Page 340: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

%sстрока,завершеннаянуль-символом%dцелоечисло(integer)%fчислоLua(double)%pуказатель%cцелоечисло(integer)каксимвол%%символ'%'Никакие модификаторы, такие как ширина или точность, ей неподдерживаются.

Иlua_concat, и lua_pushfstring полезны, когда мы хотим соединитьтолько несколько строк. Однако, если нам нужно соединить многострок(илисимволов)вместе,тоделатьэтопоодномузаразможетбытьдовольнонеэффективно,какмывиделивразделе11.6.Вэтомслучаемыможем использовать буферные средства, предоставленныевспомогательнойбиблиотекой.

При более простом варианте применения буферные средстваработаютсдвумяфункциями:однадаетвамбуферлюбогоразмера,кудавыможетеписатьсвоюстроку;другаяпреобразуетбуфервстрокуLua(Примечание: Эти две функции появились в Lua 5.2). Листинг 28.3демонстрирует использование этих функций при помощи реализациифункции string.upper прямо из исходного файла lstrlib.c. Первымшагом для использования буфера из вспомогательной библиотекиявляется объявление переменной с типом luaL_Buffer. Следующимшагом является вызов luaL_buffinitsize для получения указателя набуфер с заданным размером; затем вы можете свободно использоватьэтот буфер для создания своей строки. Последний шаг — это вызовluaL_pushresultsize для преобразования содержимого буфера в новуюстроку Lua на вершине стека. Размер в этом втором вызове — этоокончательныйразмерстроки.(Часто,каквнашемпримере,этотразмерравен размеру буфера, но он может быть меньше. Если вы не знаететочный размер получающейся строки, но знаете, что ее размерограничен, то вы можете обезопасить себя, выделив буфер большегоразмера.)

Листинг28.3.Функцияstring.upperstaticintstr_upper(lua_State*L){

size_tl;

size_ti;

luaL_Bufferb;

constchar*s=luaL_checklstring(L,1,&l);

char*p=luaL_buffinitsize(L,&b,l);

Page 341: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

for(i=0;i<l;i++)

p[i]=toupper(uchar(s[i]));

luaL_pushresultsize(&b,l);

return1;

}

Обратите внимание, что функция luaL_pushresultsize не получаетсостояние Lua в качестве своего первого аргумента. Послеинициализации буфер хранит ссылку на состояние, поэтому нам ненужно передавать его при вызове остальных функций для работы сбуферами.

Мы также можем использовать эти буферы не зная максимальнойдлины получаемой строки. Листинг 28.4 показывает упрощеннуюреализациюфункцииtable.concat.ВэтойфункциимыспервавызываемluaL_buffinit для инициализации буфера. Затем мы добавляем в буферэлементы один за другим, в этом случае используя функциюluaL_addvalue.Наконец,luaL_pushresult освобождает буфер и помещаетитоговуюстрокунавершинустека.

Листинг28.4.Упрощеннаяреализацияtable.concatstaticinttconcat(lua_State*L){

luaL_Bufferb;

inti,n;

luaL_checktype(L,1,LUA_TTABLE);

n=luaL_len(L,1);

luaL_buffinit(L,&b);

for(i=1;i<=n;i++){

lua_rawgeti(L,1,i);/*getstringfromtable*/

luaL_addvalue(b);/*addittothebuffer*/

}

luaL_pushresult(&b);

return1;

}

Вспомогательнаябиблиотекапредоставляетнесколькофункцийдлядобавлениязначенийкбуферу:функцияluaL_addvalueдобавляетстрокуLua, которая находится на вершине стека; функция luaL_addlstringдобавляетстрокисзаданнойдлиной;функцияluaL_addstringдобавляетстроку, завершеннуюнуль-символом,ифункцияluaL_addchar добавляетодиночныесимволы.Этифункцииимеютследующиепрототипы:

voidluaL_buffinit(lua_State*L,luaL_Buffer*B);

voidluaL_addvalue(luaL_Buffer*B);

voidluaL_addlstring(luaL_Buffer*B,constchar*s,size_tl);

Page 342: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

voidluaL_addstring(luaL_Buffer*B,constchar*s);

voidluaL_addchar(luaL_Buffer*B,charc);

voidluaL_pushresult(luaL_Buffer*B);

Когда вы используете буфер библиотеки auxlib, вам нужноучитывать один момент. После инициализации буфера он хранитнекоторые промежуточные результаты в стеке Lua. Поэтому вы неможете предполагать, что вершина стека останется там, где она былаперед тем, как вы начали использовать буфер. Более того, хотя выможете использовать стек для других задач во время пользованиябуфером,счетчикзаталкиваний-выталкиванийприкаждомпользованиидолженбытьсбалансированвсякийраз,когдавыобращаетеськбуферу.ИсключениемизэтогоправилаявляетсяфункцияluaL_addvalue,котораяпредполагает, что строка, которую надо добавить к буферу, былапомещенанавершинустека.

28.3.ХранениесостояниявфункцияхС

ЧастофункциямСнужнохранитькакие-нибудьнелокальныеданные,то есть данные, которые переживут вызвавшую их функцию. В С мыобычно используем глобальные (extern) или статические (static)переменные для этой цели. Однако, когда вы пишете библиотечныефункции для Lua, то использование глобальных или статическихпеременных — не очень хороший подход. Во-первых, вы не можетехранить произвольное значение Lua в переменной С. Во-вторых,библиотека, которая использует такие переменные, не будет корректноработатьснесколькимисостояниямиLua.

У функции Lua есть два основных места хранения нелокальныхданных: глобальные переменные и нелокальные переменные. С APIтакже предоставляет два основных места для хранения нелокальныхданных:реестриверхниезначения.

Реестр — это глобальная таблица, к которой может обратитьсятолькокодС.(Примечание:Насамомделекрееструможнообратитьсяииз Lua при помощи функции из отладочной библиотекиdebug.getregistry.) Обычно реестр используется для хранения данных,которые будут использоваться сразу несколькими модулями. Если вамнужносохранятьданныетолькодлявашегомодуляилифункции,товыдолжныиспользоватьверхниезначения.

Page 343: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Реестр

Реестр всегда расположен по псевдоиндексу, чье значениеопределяетсяLUA_REGISTRYINDEX.Псевдоиндекспохожнаиндексвстеке,заисключениемтого,чтосвязанныеснимзначениянаходятсяневстеке.Большинство функций в Lua API, которые принимают индексы вкачестве аргументов, также принимают и псевдоиндексы — заисключением тех функций, которые управляют стеком, например,lua_removeиlua_insert.Например,чтобыполучитьзначение,хранящеесявреестесключом"Key",мыможемиспользоватьследующийвызов:

lua_getfield(L,LUA_REGISTRYINDEX,"Key");

Реестр — это обычная таблица Lua. Соответственно, вы можетеиндексироватьеелюбымзначениемLua,кромеnil.Однако,посколькуувсех модулей С один и тот же реестр, во избежание конфликтов выдолжны с осторожностью выбирать значения для использования вкачествеключей.Строковыеключиособенноудобны,когдавыхотитеразрешить другим независимым библиотекам обращаться к вашимданным,посколькувсе,чтоимнужнознать,—этоимяключа.Длятакихключейне существует полностьюбезопасногометода выбора ключей,но есть некоторые хорошие правила, например, не использоватьраспространенные имена и начинать ваши имена с имени библиотекиили чего-то вроде этого. (Префиксы вроде lua или lualib нерекомендуются.)

Вы никогда не должны использовать числа в качестве ключейреестра, поскольку подобные ключи зарезервированы для системыссылок (reference system). Эта система состоит из пары функций вовспомогательнойбиблиотеке,которыепозволяютвамхранитьзначениявтаблице,небеспокоясьосозданииуникальныхимен.ФункцияluaL_refсоздаетновыессылки:

intr=luaL_ref(L,LUA_REGISTRYINDEX);

Данныйвызоввыталкиваетзначениеизстека,сохраняетеговтаблицесновымцелочисленнымключомивозвращаетэтотключ.Такойключмыназываемссылкой(reference).

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

Page 344: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Lua вне функции С, которая их возвратила. Более того, Lua даже непредлагает указатели на другие объекты, такие как таблицы илифункции. Поэтому мы не можем ссылаться на объекты Lua черезуказатели. Вместо этого, когда нам требуются такие указатели, мысоздаемссылкиихранимихвС.

Чтобызатолкатьзначение,связанноесссылкойr,встек,мыпростопишемследующее:

lua_rawgeti(L,LUA_REGISTRYINDEX,r);

Наконец, чтобы высвободить и значение, и ссылку, мы вызываемluaL_unref:

luaL_unref(L,LUA_REGISTRYINDEX,r);

После этого вызова новый вызов luaL_ref может снова вернуть этуссылку.

Система ссылок трактует nil как особый случай.Каждыйраз, когдамывызываемluaL_ref для значения nil, вместо создания новой ссылкиона возвращает ссылку на константу lua_refnil. Следующий вызовничегонеделает:

luaL_unref(L,LUA_REGISTRYINDEX,LUA_REFNIL);

Аэтотвызовзаталкиваетnil,какиожидалось:lua_rawgeti(L,LUA_REGISTRYINDEX,LUA_REFNIL);

Система ссылок также определяет константу LUA_NOREF, котораяявляетсяцелымчислом,отличнымотлюбойдействующейссылки.Онаудобна,чтобыпомечатьссылкикакнедействительные.

Другим безопасным методом создания ключей в реестре являетсяиспользование в качестве ключа адреса статической переменной ввашемкоде: компоновщикпозаботится о том, чтобы этот адресбудетуникальнымсредивсехбиблиотек.Дляиспользованияданноговариантавам понадобится функция lua_pushlightuserdata, которая заталкивает встек Lua значение, представляющее указатель С. Следующий кодпоказывает, как хранить и возвращать строку из реестра при помощиэтогометода:

/*переменнаясуникальнымадресом*/

staticcharKey='k';

/*хранитстроку*/

Page 345: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

lua_pushlightuserdata(L,(void*)&Key);/*заталкиваетадресс*/

lua_pushstring(L,myStr);/*заталкиваетзначение*/

lua_settable(L,LUA_REGISTRYINDEX);/*registry[&Key]=myStr*/

/*возвращаетстроку*/

lua_pushlightuserdata(L,(void*)&Key);/*заталкиваетадрес*/

lua_gettable(L,LUA_REGISTRYINDEX);/*возвращаетзначение*/

myStr=lua_tostring(L,-1);/*преобразуетеговстроку*/

Мыобсудимоблегченныепользовательскиеданные(lightuserdata)болееподробновразделе29.5.

Чтобы упростить использование адресов переменных в качествеуникальных ключей, Lua 5.2 вводит две новые функции: lua_rawgetp иlua_rawsetp.Онипохожинаlua_rawgetiиlua_rawseti, но вместо целыхчисел они используют как ключи указатели C (переведенные воблегченные пользовательские данные). С ними мы можем переписатьпредыдущийкодследующимобразом:

staticcharKey='k';

/*хранитстроку*/

lua_pushstring(L,myStr);

lua_rawsetp(L,LUA_REGISTRYINDEX,(void*)&Key);

/*возвращаетстроку*/

lua_rawgetp(L,LUA_REGISTRYINDEX,(void*)&Key);

myStr=lua_tostring(L,-1);

Обе функции используют прямой доступ. Так как у реестра нетметатаблицы,прямойдоступведетсебятакже,какиобычныйдоступ,нонемногоэффективнее.

Верхниезначения

Втовремякакреестрпредлагаетглобальныепеременные,механизмверхнихзначений(upvalue)реализуетаналогстатическихпеременныхвС,которыевиднытольковнутриконкретнойфункции.Каждыйраз,когдавы создаете новую функцию С в Lua, вы можете связать с ней любоеколичество верхних значений; каждое верхнее значениеможет хранитьоднозначениеLua.Потомпривызовефункциионаполучаетсвободныйдоступклюбомуизееверхнихзначений,используяпсевдоиндексы.

Мы называем эту связь функцииС со своими верхними значениямизамыканием (closure). ЗамыканиеСпримерносоответствует замыканиюLua. В частности, вы можете создавать разные замыкания, используя

Page 346: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

одинитотжекодфункции,носразнымиверхнимизначениями.ВкачествепростогопримерадавайтенапишемфункциюnewCounter

наС.(Примечание:МыопределилиэтужефункциюнаLuaвразделе6.1.)Этафункцияявляетсяфабрикой:онавозвращаетновуюфункциюcounterприкаждомсвоемвызове.ХотяувсехэтихфункцийодинитотжекодС, каждая из них хранит свой собственный счетчик. Эта функция-фабрикавыглядитследующимобразом;

staticintcounter(lua_State*L);/*предваряющееобъявление*/

intnewCounter(lua_State*L){

lua_pushinteger(L,0);

lua_pushcclosure(L,&counter,1);

return1;

}

Главной функцией здесь является lua_pushcclosure, которая создаетновое замыкание. Ее вторым аргументом является базовая функция (впримере этоcounter), а третьим—число верхних значений (в примереэто1).Передсозданиемновогозамыканиямыдолжнызатолкатьвстекначальные значения для их верхних значений. В нашем примере мызаталкиваем 0 как начальное значение для единственного верхнегозначения.Какиожидалось,lua_pushcclosureоставляетновоезамыканиев стеке, поэтому замыкание уже готово к возвращению как результатnewCounter.

Теперьдавайтерассмотримопределениеcounter:staticintcounter(lua_State*L){

intval=lua_tointeger(L,lua_upvalueindex(1));

lua_pushinteger(L,++val);/*новоезначение*/

lua_pushvalue(L,-1);/*дублируетего*/

lua_replace(L,lua_upvalueindex(1));/*обновляетверхнеезначение

*/

return1;/*возвращаетновоезначение

*/

}

Здесь ключевым элементом является макрос lua_upvalueindex, которыйпроизводит псевдоиндекс верхнего значения. В частности, выражениеlua_upvalueindex(1)возвращаетпсевдоиндекспервоговерхнегозначениявыполняющейся функции. Опять же, этот псевдоиндекс выглядит каклюбой другой индекс стека, за исключением того, что он не живет встеке. Поэтому вызов lua_tointeger возвращает текущее значениепервого(иединственного)верхнегозначениякакчисло.Затемфункция

Page 347: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

counter заталкивает в стек новое значение ++val, делает его копию ииспользуетоднуизкопий,чтобызаменить значениеверхнего значения.Наконец,онавозвращаетдругуюкопиюкаксвоевозвращаемоезначение.

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

x=tuple.new(10,"hi",{},3)

print(x(1))-->10

print(x(2))-->hi

print(x())-->10hitable:0x80878783

ВязыкеСмыпредставляемвсекортежиприпомощиоднойитойжефункцииt_tuple,продемонстрированнойвлистинге28.5.Посколькумыможем вызвать кортеж как с числовым аргументом, так и вообще безаргументов, функция t_tuple использует luaL_optint для получениянеобязательного аргумента. Функция luaL_optint похожа наluaL_checkint, но она не жалуется на отсутствие аргумента — онавозвращаетзаданноезначениепоумолчанию(впримереэто0).

Листинг28.5.Реализациякортежейintt_tuple(lua_State*L){

intop=luaL_optint(L,1,0);

if(op==0){/*нетаргументов?*/

inti;

/*заталкиваеткаждоедопустимоеверхнеезначениевстек*/

for(i=1;!lua_isnone(L,lua_upvalueindex(i));i++)

lua_pushvalue(L,lua_upvalueindex(i));

returni-1;/*числозначенийвстеке*/

}

else{/*получаетполе'op'*/

luaL_argcheck(L,0<op,1,"indexoutofrange");

if(lua_isnone(L,lua_upvalueindex(op)))

return0;/*такогополянет*/

lua_pushvalue(L,lua_upvalueindex(op));

return1;

}

}

Page 348: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

intt_new(lua_State*L){

lua_pushcclosure(L,t_tuple,lua_gettop(L));

return1;

}

staticconststructluaL_Regtuplelib[]={

{"new",t_new},

{NULL,NULL}

};

intluaopen_tuple(lua_State*L){

luaL_newlib(L,tuplelib);

return1;

}

Когда мы индексируем несуществующее верхнее значение, торезультатом является псевдозначение типа LUA_TNONE. (Когда мыобращаемсякиндексустека,находящемусянадвершинойстека,томытакжеполучаемпсевдозначениетипаLUA_TNONE.)Поэтомунашафункцияt_tupie использует lua_isnone для проверки, что у нее есть заданноеверхнее значение. Однако, мы никогда не должны вызыватьlua_upvalueindex с отрицательным индексом, поэтому нам следуетпроверять данное условие, когда пользователь передает индекс.Функция luaL_argcheck проверяет любое заданное условие, вызываяошибку,еслипотребуется.

Функция для создания кортежей, t_new (тоже в листинге 28.5),тривиальна: поскольку все ее аргументы уже в стеке, ей всего лишьнужно вызвать lua_pushcclosure для создания замыкания из t_tuple совсеми своими аргументами в качестве верхних значений. Наконец,массивtuplelibифункцияluaopen_tuple(тожевлистинге28.5)являютсястандартным кодом для создания библиотеки tuple с единственнойфункциейnew.

Общиеверхниезначения

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

В отличие от замыканий Lua, замыкания С не могут иметь общиеверхние значения. У каждого замыкания имеются свои собственныенезависимые верхние значения. Однако, мы можем настроить верхние

Page 349: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

В Lua 5.2 предлагает функцию, которая облегчает задачусовместногоиспользованияверхнегозначениямеждувсемифункциямибиблиотеки.МыоткрывалибиблиотекиСприпомощиluaL_newlib. Luaреализуетэтуфункциювкачествеследующегомакроса:

#defineluaL_newlib(L,l)\

(luaL_newlibtable(L,l),luaL_setfuncs(L,l,0))

Макрос luaL_newlibtable просто создает таблицу для этой библиотеки.(Мы также могли бы использовать lua_newtable, но этот макросиспользуетlua_createtableдлясозданиятаблицызаранееопределенногоразмера, оптимального для количествафункций в данной библиотеке.)Функция luaL_setfuncs добавляет функции из списка l в эту новуютаблицу,котораянаходитсянавершинестека.

Здесь нас интересует третий параметр функции luaL_setfuncs. Онсообщает, сколько верхних значений будут иметь новые функции вбиблиотеке. Начальные значения для этих верхних значений должнынаходитьсявстеке,какивслучаесlua_pushcclosure.Такимобразом,длясоздания библиотеки, где все функции будут совместно использоватьобщую таблицу как свое единственное верхнее значение, мы можемиспользоватьследующийкод:

/*создаетбиблиотечнуютаблицу('lib'—этоеесписокфункций)*/

luaL_newlibtable(L,lib);

/*создаетобщееверхнеезначение*/

lua_newtable(L);

/*добавляетфункцииксписку'lib'дляновойбиблиотеки,

совместноиспользуяпрежнюютаблицукакверхнеезначение*/

luaL_setfuncs(L,lib,1);

Последнийвызовтакжеудаляетобщуютаблицуизстека,оставляятамтольконовуюбиблиотеку.

Упражнения

Упражнение28.1.РеализуйтенаCфункциюфильтрации(filter).Онадолжнаполучатьсписокипредикат,азатемвозвращатьновыйсписоксовсеми элементами из заданного списка, которые удовлетворяютзаданномупредикату:

Page 350: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

t=filter({1,3,20,-4,5},function(x)returnx<5end)

--t={1,3,-4}

(Предикат — это всего лишь функция, которая проверяет какое-либоусловиеивозвращаетлогическоезначение.)

Упражнение28.2.Изменитефункциюl_split (из листинга28.2) так,чтобы она могла работать со строками, содержащими нуль-символы.(Кромепрочихизменений,онатакжедолжнаиспользоватьmemchrвместоstrchr.)

Упражнение 28.3. Заново реализуйте функцию transliterate

(упражнение21.3)наС.Упражнение 28.4. Реализуйте библиотеку с измененной функцией

transliterate так, чтобы таблица транслитерации не передавалась какаргумент, а хранилась самой библиотекой. Ваша библиотека должнапредоставитьследующиефункции:

lib.settrans(table)--устанавливаеттаблицутранслитерации

lib.gettrans()--получаеттаблицутранслитерации

lib.tranliterate(s)--транслитерирует's'всоответствиис

текущейтаблицей

Используйтереестрдляхранениятаблицытранслитерации.Упражнение28.5.Повторитепредыдущееупражнение,используядля

хранениятаблицытранслитерацииверхнеезначение.Упражнение 28.6. Считаете ли вы хорошим стилем разработки

хранитьтаблицутранслитерациикакчастьсостояниябиблиотеки,анепередаватьеекакпараметрвtransliterate?

Page 351: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА29

ЗадаваемыепользователемтипывС

В предыдущей главе мы увидели, как расширить Lua при помощиновыхфункций,написанныхнаС.Теперьмыувидим,какрасширитьLuaприпомощиновых типов, написанныхнаС.Мыначнем снебольшогопримера; на протяжении всей главы мы будем расширять его припомощиметаметодовипрочихвкусняшек.

Наш пример для запуска довольно прост: массив логическихзначений.Такаяпростаяструктуравыбранапотому,чтоейнетребуютсякакие-тосложныеалгоритмы,поэтомумысможемсосредоточитьсянааспектахAPI.Темнеменее,этотпримерполезенсампосебе.Разумеется,в Lua мы можем использовать таблицы для реализации массивовлогическихзначений.НореализациянаС,гдемыхранимкаждуюзаписьв одном единственном бите, использует менее 3% от памяти,используемойдлятаблицы.

Длянашейреализациинампонадобятсяследующиеопределения:#include<limits.h>

#defineBITS_PER_WORD(CHAR_BIT*sizeof(unsignedint))

#defineI_WORD(i)((unsignedint)(i)/BITS_PER_WORD)

#defineI_BIT(i)(1<<((unsignedint)(i)%BITS_PER_WORD))

BITS_PER_WORD—этоколичествобитвбеззнаковомцеломчисле.МакросI_WORD вычисляет слово, которое хранит бит по заданному индексу, амакрос I_BIT вычисляет битовую маску для обращения ксоответственномубитуданногослова.

Мы будем представлять наши массивы при помощи следующейструктуры:

typedefstructNumArray{

intsize;

unsignedintvalues[1];/*переменнаячасть*/

}NumArray;

Мы объявляем массив values с размером в 1 лишь в качествезаполнителя,посколькустандартC89недопускаетмассивысразмером0;мывыставимегонастоящийразмер,когдавыделимдлянегопамять.

Page 352: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Следующее выражение вычисляет итоговый размер массива с n

элементами:sizeof(NumArray)+I_WORD(n-1)*sizeof(unsignedint)

(Мывычлиединицуизn,посколькуначальнаяструктураужесодержитместопододинэлемент.)

29.1.Пользовательскиеданные(userdata)

Нашаперваязадача—понять,какпредставитьструктуруNumArrayвLua. Специально для этого Lua предоставляет базовый тип:пользовательскиеданные (userdata). Данный тип соответствует областинеобрабатываемой памяти в Lua без каких-либо встроенных для нееопераций,вкотороймыможемхранитьчто-угодно.

Функция lua_newuserdata выделяет блок памяти заданного размера,заталкивает соответственные пользовательские данные в стек ивозвращаетадресэтогоблока:

void*lua_newuserdata(lua_State*L,size_tsize);

Еслипокакой-топричиневамнужновыделитьпамятьинымспособом,то можно очень легко создать пользовательские данные размером суказатель и хранить там указатель на настоящий блок памяти. Мыувидимпримерыданнойтехникивглаве30.

С помощью lua_newuserdata функция для создания новых массивовлогическихзначенийвыглядитследующимобразом:

staticintnewarray(lua_State*L){

inti;

size_tnbytes;

NumArray*a;

intn=luaL_checkint(L,1);

luaL_argcheck(L,n>=1,1,"invalidsize");

nbytes=sizeof(NumArray)+I_WORD(n-1)*sizeof(unsignedint);

a=(NumArray*)lua_newuserdata(L,nbytes);

a->size=n;

for(i=0;i<=I_WORD(n-1);i++)

a->values[i]=0;/*инициализируетмассив*/

return1;/*новыепользовательскиеданныеужевстеке*/

}

Page 353: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

(Макрос luaL_checkint — это лишь приведение типа надluaL_checkinteger.)КактолькофункцияnewarrayзарегистрированавLua,мыможемсоздаватьновыемассивыприпомощиоператоровнаподобиеа=аггау.new(1000).

Для сохранения записи мы будем использовать вызов вродеarray.set(a,index,value). Позже мы увидим, как использоватьметатаблицыдляподдержкиболееудобногосинтаксисаa[index]=value.В обоих формах записи в основе лежит одна и та же функция. Онапредполагает,чтоиндексыначинаютсяс1,какэтопринятовLua:

staticintsetarray(lua_State*L){

NumArray*a=(NumArray*)lua_touserdata(L,1);

intindex=luaL_checkint(L,2)-1;

luaL_argcheck(L,a!=NULL,1,"'array'expected");

luaL_argcheck(L,0<=index&&index<a->size,2,

"indexoutofrange");

luaL_checkany(L,3);

if(lua_toboolean(L,3))

a->values[I_WORD(index)]|=I_BIT(index);/*setbit*/

else

a->values[I_WORD(index)]&=~I_BIT(index);/*resetbit*/

return0;

}

ПосколькуLua в качестве логическогопринимает любое значение, длятретьего параметра мы используем luaL_checkany: она лишь проверяет,что для данного параметра есть значение (не важно, какое). Если мывызовемsetarrayснекорректнымиаргументами,тополучимподробныесообщенияобошибках:

array.set(0,11,0)

-->stdin:1:badargument#1to'set'('array'expected)

array.set(a,1)

-->stdin:1:badargument#3to'set'(valueexpected)

Следующаяфункциявозвращаетзапись:staticintgetarray(lua_State*L){

NumArray*a=(NumArray*)lua_touserdata(L,1);

intindex=luaL_checkint(L,2)-1;

luaL_argcheck(L,a!=NULL,1,"'array'expected");

luaL_argcheck(L,0<=index&&index<a->size,2,

"indexoutofrange");

lua_pushboolean(L,a->values[I_WORD(index)]&I_BIT(index));

Page 354: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

return1;

}

Мыопределимдругуюфункциюдлявозвращенияразмерамассива:staticintgetsize(lua_State*L){

NumArray*a=(NumArray*)lua_touserdata(L,1);

luaL_argcheck(L,a!=NULL,1,"'array'expected");

lua_pushinteger(L,a->size);

return1;

}

Наконец,намнужнонемногодополнительногокодадляинициализациинашейбиблиотеки:

staticconststructluaL_Regarraylib[]={

{"new",newarray},

{"set",setarray},

{"get",getarray},

{"size",getsize},

{NULL,NULL}

};

intluaopen_array(lua_State*L){

luaL_newlib(L,arraylib);

return1;

}

И вновь мы воспользуемся функцией luaL_newlib из вспомогательнойбиблиотеки. Она создает таблицу и заполняет ее парами имя-функция,заданнымимассивомarraylib.

ПослеоткрытиябиблиотекимыготовыиспользоватьнашновыйтипвLua:

a=array.new(1000)

print(a)-->userdata:0x8064d48

print(array.size(a))-->1000

fori=1,1000do

array.set(a,i,i%5==0)

end

print(array.get(a,10))-->true

29.2.Метатаблицы

У нашей текущей реализации есть крупная дыра в безопасности.Допустим, пользователь напишет что-то вродеarray.set(io.stdin,l,false).Значениевio.stdin—этопользовательскиеданные с указателем на поток (FILE*). Из-за того, что это

Page 355: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

пользовательские данные, array.set с радостью примет их какдопустимыйаргумент;вероятно,результатомбудетповреждениепамяти(если повезет, вы можете получить вместо этого сообщение о выходеиндексазаграницыиндексации).Подобноеповедениенедопустимодлялюбой библиотеки Lua. Независимо от того, как вы используетебиблиотеку,онанедолжнаповреждатьданныеCилиприводитьксбоямсвыдачейдампаядраизLua.

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

Нам также нужно место для хранения этой новой метатаблицы,чтобы мы могли обращаться к ней при создании новыхпользовательских данных и при проверке того, что у требуемыхпользовательскихданныхправильныйтип.Какмыужевидели,естьдвавариантадляхраненияметатаблицы:вреестреиликакверхнеезначениедля функций в библиотеке. В Lua принято регистрировать каждыйновый типС в реестре, используяимятипа как индекс, а метатаблицукак его значение. Как и с любыми другими индексами реестра, воизбежание конфликтов мы должны с осторожностью выбирать имятипа.Внашемпримеремыбудемиспользоватьдлянашегоновоготипаимя"LuaBook.array".

Как обычно, в этом нам поможет вспомогательная библиотека,предоставивнекоторыефункции.Этиновыевспомогательныефункции,которымимыбудемпользоваться,следующие:

intluaL_newmetatable(lua_State*L,constchar*tname);

voidluaL_getmetatable(lua_State*L,constchar*tname);

void*luaL_checkudata(lua_State*L,intindex,

constchar*tname);

ФункцияluaL_newmettable создает новую таблицу (для использования вкачествеметатаблицы), помещает еена вершину стекаи связывает этутаблицу с заданным именем в реестре. Функция luaL_getmetatable

возвращаетметатаблицу,связаннуюсименемtname,изреестра.Наконец,luaL_checkudata проверяет, что объект на заданной позиции стека

Page 356: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

является пользовательскими данными с метатаблицей, котораясоответствует заданномуимени.Она вызывает ошибку, если у объектанеправильная метатаблица, или это не пользовательские данные; впротивномслучаеонавозвращаетадресобъекта.

Теперь мы можем начать нашу реализацию. Первым шагом будетизменениефункции, котораяоткрываетнашубиблиотеку.Новаяверсиядолжнасоздаватьметатаблицудлямассивов:

intluaopen_array(lua_State*L){

luaL_newmetatable(L,"LuaBook.array");

luaL_newlib(L,arraylib);

return1;

}

Следующимшагомбудетизменениеnewarray такимобразом, чтобыонаустанавливаламетатаблицувовсехсоздаваемыхеймассивах:

staticintnewarray(lua_State*L){

<какпрежде>luaL_getmetatable(L,"LuaBook.array");

lua_setmetatable(L,-2);

return1;/*новыепользовательскиеданныеужевстеке*/

}

Функцияlua_setmetatableвыталкиваеттаблицуизстекаиустанавливаетеекакметатаблицудляобъектапозаданномуиндексу.Внашемслучаеэтимобъектомявляютсяновыепользовательскиеданные.

Наконец, setarray, getarray и getsize должны проверить, что ониполучили допустимый массив в качестве своего первого аргумента.Чтобыупроститьимзадачи,мыопределимследующиймакрос:

#definecheckarray(L)\

(NumArray*)luaL_checkudata(L,1,"LuaBook.array")

С помощью этого макроса новое определение getsize не вызываетзатруднений:

staticintgetsize(lua_State*L){

NumArray*a=checkarray(L);

lua_pushinteger(L,a->size);

return1;

}

Поскольку setarray и getarray к тому же имеют общий код дляпроверкииндексакаксвоеговторогоаргумента,мывынесемихобщие

Page 357: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

частивследующуюфункцию:staticunsignedint*getindex(lua_State*L,unsignedint*mask){

NumArray*a=checkarray(L);

intindex=luaL_checkint(L,2)-1;

luaL_argcheck(L,0<=index&&index<a->size,2,

"indexoutofrange");

/*возвращаетадресэлемента*/

*mask=I_BIT(index);

return&a->values[I_WORD(index)];

}

Послеопределенияgetindexнесложнонаписатьsetarrayиgetarray:staticintsetarray(lua_State*L){

unsignedintmask;

unsignedint*entry=getindex(L,&mask);

luaL_checkany(L,3);

if(lua_toboolean(L,3))

*entry|=mask;

else

*entry&=~mask;

return0;

}

staticintgetarray(lua_State*L){

unsignedintmask;

unsignedint*entry=getindex(L,&mask);

lua_pushboolean(L,*entry&mask);

return1;

}

Теперь, если вы попытаетесь выполнить что-то вродеarray.get(io.stdin,10), то получите соответственное сообщение обошибке:

error:badargument#1to'get'('array'expected)

29.3.Объектно-ориентированныйдоступ

Нашимследующимшагомбудетпреобразованиенашегоновоготипав объект, чтобы мы могли работать с его экземплярами при помощиобычногообъектно-ориентированногосинтаксисатакимобразом:

a=array.new(1000)

print(a:size())-->1000

Page 358: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

a:set(10,true)

print(a:get(10))-->true

Вспомним, что форма записи a:size() эквивалента a.size(а).Поэтому мы должны переделать выражение а.size так, чтобы оновозвращало нашу функцию getsize. Ключевым механизмом здесьявляется метаметод __index. Lua вызывает этот метаметод для таблицвсякийраз,когдаоннеможетнайтизначениедлязаданногоключа.Дляпользовательских данных Lua вызывает его при каждом обращении,посколькуупользовательскихданныхвообщенетключей.

Предположим,чтомывыполнилиследующийкод:localmetaarray=getmetatable(array.new(1))

metaarray.__index=metaarray

metaarray.set=array.set

metaarray.get=array.get

metaarray.size=array.size

В первой строке кода мы создаем массив лишь для получения егометатаблицы, которую мы присваиваем metarray. (Мы не можемустановитьметатаблицупользовательскихданныхизLua,номыможемполучить ее.) Затем мы устанавливаем metaarray.__index равнойmetaarray.Когдамывычисляемa.size,Luaнеможетнайтиключ"size"вобъектеа,посколькуэтотобъектявляетсяпользовательскимиданными.Поэтому Lua пытается получить это значение из поля __index

метатаблицыа, которая совпадает с самимmetaarray. Но metaarray.size— это array.size, поэтому а.size(а) возвращает array.size(а), как итребовалось.

Конечно,мыможемнаписать тоже самоенаС.Мыможемсделатьеще лучше: теперь, когда массивы являются объектами со своимисобственнымиоперациями,намбольшененужнохранитьэтиоперациивтаблицеarray.Единственнойфункцией,которуюнашабиблиотекапо-прежнему должна экспортировать, является функция new для созданияновыхмассивов.Всепрочиеоперациидоступнытолькокакметоды.ВсвязисэтимкодСможетрегистрироватьихнапрямую.

Операцииgetsize, getarray и setarray останутся такимиже, как и внашем предыдущем подходе. Изменится только то, как мы ихзарегистрируем.Дляэтогонамнужноизменитькод,которыйоткрываетбиблиотеку. Во-первых, нам потребуются два отдельных спискафункций:списокобычныхфункцийисписокметодов.

staticconststructluaL_Regarraylib_f[]={

Page 359: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

{"new",newarray},

{NULL,NULL}

};

staticconststructluaL_Regarraylib_m[]={

{"set",setarray},

{"get",getarray},

{"size",getsize},

{NULL,NULL}

};

Новая версия открывающей функции luaopen_array должна создатьметатаблицу, присвоить ее своему собственному полю __index,зарегистрировать тамвсеметоды, а затемсоздатьи заполнить таблицуarray:

intluaopen_array(lua_State*L){

luaL_newmetatable(L,"LuaBook.array");

/*metatable.__index=metatable*/

lua_pushvalue(L,-1);/*дублируетэтуметатаблицу*/

lua_setfield(L,-2,"__index");

luaL_setfuncs(L,arraylib_m,0);

luaL_newlib(L,arraylib_f);

return1;

}

Здесь мы вновь используем luaL_setfuncs, чтобы записать функции изсписка arraylib_m в метатаблицу, которая находится на вершине стека.Затем мы используем luaL_newlib для создания новой таблицы ирегистрациитамфункцийизспискаarraylib_f (вданномслучаетолькоnew).

В качестве завершающего штриха мы добавим к нашему новомутипуметод__tostring,чтобыprint(а)печатал"array"иразмермассивавкруглых скобках; должно получиться что-то вроде "array(1000)".Нижесамафункция:

intarray2string(lua_State*L){

NumArray*a=checkarray(L);

lua_pushfstring(L,"array(%d)",a->size);

return1;

}

Вызов lua_pushfstring форматирует строку и оставляет ее на вершинестека. Мы также должны добавить array2string к списку аггауlib_m,чтобывключитьеевметатаблицуизобъектов-массивов:

Page 360: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

staticconststructluaL_Regarraylib_m[]={

{"__tostring",array2string},

<другиеметоды>};

29.4.Доступкаккмассиву

Альтернативой объектно-ориентированной нотации являетсяобычнаянотациядлядоступакнашиммассивам.Вместозаписиа:get(i)мымоглибыпростописатьа[i].Длянашегопримераэтосделатьлегко,поскольку наши функции setarray и getarray уже получают своиаргументывтомпорядке,вкоторомонизаданыдлясоответствующихметаметодов.БыстрымрешениембудетопределениеэтихметаметодовпрямовнашемкодеLua:

localmetaarray=getmetatable(array.new(1))

metaarray.__index=array.get

metaarray.__newindex=array.set

metaarray.__len=array.size

(Мыдолжнывыполнитьэтоткоднанашейпервоначальнойреализациимассивов, без модификаций для объектно-ориентированного доступа.)Этовсе,чтонамнужнодляиспользованиястандартногосинтаксиса:

a=array.new(1000)

a[10]=true--'setarray'

print(a[10])--'getarray'-->true

print(#a)--'getsize'-->1000

Если потребуется, мы можем зарегистрировать эти метаметоды вкодеС.Дляэтогомысновадолжныизменитьнашуинициализирующуюфункцию:

staticconststructluaL_Regarraylib_f[]={

{"new",newarray},

{NULL,NULL}

};

staticconststructluaL_Regarraylib_m[]={

{"__newindex",setarray},

{"__index",getarray},

{"__len",getsize},

{"__tostring",array2string},

{NULL,NULL}

};

intluaopen_array(lua_State*L){

Page 361: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

luaL_newmetatable(L,"LuaBook.array");

luaL_setfuncs(L,arraylib_m,0);

luaL_newlib(L,arraylib_f);

return1;

}

Вэтойновойверсииунасопятьестьлишьоднаоткрытаяфункция—new. Все прочие функции доступны только как метаметоды дляспецифическихопераций.

29.5.Облегченныепользовательскиеданные

Видпользовательскихданных,которыймыдосихпориспользовали,называется полными пользовательскими данными (ППД) (full userdata).Lua предлагает и другой вид— облегченные пользовательские данные(ОПД)(lightuserdata).

ТипОПДпредставляетсобойпростоуказательвС(тоестьзначениетипаvoid*).Этозначение,анеобъект;мынесоздаемего(также,какмыне создаем числа). Чтобы поместить ОПД в стек, мы вызываемlua_pushlightuserdata:

voidlua_pushlightuserdata(lua_State*L,void*p);

Несмотря на похожие имена, полные и облегченныепользовательскиеданныенасамомделесильноотличаются.ОПД—этоне буферы, а всего лишь указатели. У них нет метатаблиц. Сборщикмусораихнеудаляет,такжекакичисла.

Иногда мы используем ОПД как дешевую альтернативу ППД.Однако, это не типичное их применение. Во-первых, у ОПД нетметатаблиц,поэтомумынеможемзнатьихтип.Во-вторых,несмотрянасвое название, ППД тоже довольно дешевы. Они лишь немногоснижаютпроизводительностьпосравнениюсвызовомmalloc.

Настоящее применениеОПД связано с проверкой на равенство. Таккак типППД является объектом, то он равен только самому себе. ТипОПД,сдругойстороны,представляетизсебязначениеуказателяC.Икактаковой он равен любым пользовательским данным, представляющимтотже самый указатель. Таким образом, мыможем использоватьОПДдлянахожденияобъектовСвнутриLua.

Мыужевиделитипичноеприменениеоблегченныхпользовательскихданныхкакключейвреестре(см.раздел28.3).ТамравенствоОПДбылокрайне важным. Каждый раз, когда мы заталкиваем ОПД в стек при

Page 362: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

помощи lua_pushlightuserdata, мы получаем то же значение Lua и,соответственно,тужезаписьвреестре.

Другим типичным сценарием является необходимость получитьполные пользовательские данные по их адресу в С. Допустим, мыорганизуемсвязьмеждуLuaиграфическойоконнойсистемой.Тогдамыможем использовать ППД для представления окон. Каждыепользовательскиеданныесодержатиливсюструктуру,представляющуюокно, или только указатель на структуру, созданную системой. Когдавнутри окна случается событие (например, нажатие кнопки мыши),система вызывает соответствующий обработчик, идентифицируя окнопоегоадресу.ЧтобыпередатьвLuaобратныйвызов,мыдолжнынайтипользовательские данные, представляющие данное окно. Чтобы ихнайти, мы можем использовать таблицу, где индексы — это ОПД садресамиокон,азначения—этоППД,представляющиеэтиокнавLua.Если у нас есть адрес окна, мы заталкиваем его в стек как ОПД ииспользуемихкакиндексв тойтаблице. (Скореевсего,у тойтаблицыдолжны быть слабые значения. Иначе те полные пользовательскиеданныеникогданебудутудаленысборщикоммусора.)

Упражнения

Упражнение 29.1. Измените реализацию setarray, чтобы онапринималатолькологическиезначения.

Упражнение 29.2. Мы можем рассматривать массив логическихзначенийкакмножествоцелыхчисел(индексысистиннымизначениямив массиве). Добавьте к реализации массивов логических значенийфункции, которые вычисляют объединение и пересечение двухмассивов. Эти функции должны получать два массива и возвращатьновыймассив,неизменяясвоипараметры.

Упражнение 29.3. Измените реализацию метаметода __tostring,чтобыонпоказывалполноесодержимоемассиваподходящимобразом.Используйтебуферныесредства(см.раздел28.2)длясозданияитоговойстроки.

Упражнение 29.4. На основе примера с массивами логическихзначенийреализуйтебиблиотекуСдляработысмассивамицелыхчисел.

Page 363: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА30

Управлениересурсами

В нашей реализации массивов логических значений из предыдущейглавы мы не беспокоились об управлении ресурсами. Этим массивамнужналишьпамять.Каждыепользовательскиеданные,представляющиемассив, обладают своей собственнойпамятью, которой управляетLua.Когдамассивстановитсямусором(тоестьстановитсянедоступнымдляпрограммы),Luaсовременемутилизируетегоиосвободитзанимаемуюимпамять.

Ножизньневсегдатаклегка.Иногдаобъектунужныдругиересурсы,кроме необработанной памяти, такие как дескрипторы файлов,дескрипторыоконит.п.(зачастуюэтиресурсытожеявляютсяпамятью,но управляются другой частью системы). В подобных случаях, когдаобъект становится мусором и утилизируется, необходимо как-товысвободитьиэтиресурсы.

Какмыужевиделивразделе17.6,Luaпредоставляетфинализаторыввиде метаметода __gc. Чтобы проиллюстрировать применение этогометаметода в С и API в целом, в данной главе мы разработаем двепривязки от Lua к внешним средствам. Первый пример — это инаяреализация функции для обхода директории. Второй (и болеесущественный) пример — это привязка к Expat, парсеру XML соткрытым исходным кодом. (Парсер — это программа длясинтаксическогоанализа.)

30.1.Итераторподиректории

В разделе 27.1 мы реализовали функцию dir, которая возвращалатаблицу со всеми файлами из заданной директории. Наша новаяреализация вернет итератор, который возвращает новую запись прикаждом вызове. С этой новой реализацией мы можем перебратьдиректориюпосредствомпримернотакогоцикла:

forfnameindir.open(".")do

print(fname)

end

Page 364: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Для итерации по директории в С нам понадобится структура DIR.Экземпляры DIR нужно создавать при помощи opendir и явновысвобождать вызовом closedir. Наша предыдущая реализация dir

храниласвойэкземплярDIRкаклокальнуюпеременнуюизакрывалаэтотэкземпляр после получения имени последнего файла. Наша новаяреализация dir не может хранить этот экземпляр DIR в локальнойпеременной, поскольку она должна запрашивать это значение напротяжении нескольких вызовов. Более того, она не может закрытьдиректорию, не получив ее последнее имя; если программа прерветцикл, итератор никогда не получит ее последнее имя. Поэтому, чтобыубедиться,чтоэкземплярDIRвсегдавысвобожден,мыхранимееадресвпользовательских данных и используем метаметод __gc длявысвобожденияструктурыэтойдиректории.

Несмотря на свою центральную роль в нашей реализации, этимпользовательским данным, представляющим директорию, не нужнобыть видимыми из Lua. Функция dir возвращает итерирующуюфункцию, которую и видит Lua. Директория может быть верхнимзначением этой итерирующей функции. Таким образом, итерирующаяфункцияобладаетпрямымдоступомкэтойструктуре,нокодLuaкнейдоступанеимеет(иемуэтоненужно).

Итого нам нужны три функции С. Во-первых, нам нужна функцияdir.open — средство, которое Lua вызывает для создания итераторов;она должна открыть структуру DIR и создать замыкание итерирующейфункции с этой структурой в качестве верхнего значения. Во-вторых,намнужнаитерирующаяфункция.В-третьих,намнуженметаметод__gc,которыйзакрываетструктуруDIR.Как обычно, нам такжепонадобитсядополнительная функция для начальной подготовки, например, длясозданияиинициализацииметатаблицыдлядиректорий.

Давайтеначнемнашкодсфункцииdir.open,показаннойвлистинге30.1. У нее есть один нюанс— она должна создать пользовательскиеданныедооткрытиядиректории.Еслионасначалаоткроетдиректорию,то вызовlua_newuserdata приведет к ошибке доступа к памяти, потерефункциииутечкеструктурыDIR.ПриправильномпорядкеструктураDIR,как только она создана, сразу же привязывается к пользовательскимданным;чтобынислучилосьпослеэтого,метаметод__gcсовременемвысвободитэтуструктуру.

Листинг30.1.Фабричнаяфункцияdir.open

Page 365: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

#include<dirent.h>

#include<errno.h>

#include<string.h>

#include"lua.h"

#include"lauxlib.h"

/*предваряющееобъявлениедляитерирующейфункции*/

staticintdir_iter(lua_State*L);

staticintl_dir(lua_State*L){

constchar*path=luaL_checkstring(L,1);

/*создаетобъектuserdataдляхраненияадресаDIR*/

DIR**d=(DIR**)lua_newuserdata(L,sizeof(DIR*));

/*устанавливаетегометатаблицу*/

luaL_getmetatable(L,"LuaBook.dir");

lua_setmetatable(L,-2);

/*пытаетсяоткрытьзаданнуюдиректорию*/

*d=opendir(path);

if(*d==NULL)/*ошибкаоткрытиядиректории?*/

luaL_error(L,"cannotopen%s:%s",path,strerror(errno));

/*создаетивозвращаетитерирующуюфункцию;

ееединственноеверхнеезначение,

пользовательскиеданныедиректории,

уженаходитсянавершинеэтогостека*/

lua_pushcclosure(L,dir_iter,1);

return1;

}

Следующаяфункция—этоdir_iter(листинг30.2),самитератор.Еекоднетребуетдолгихпояснений.ОнаполучаетадресструктурыDIRизее верхнего значения и вызывает readdir для получения следующейзаписи.

Функция dir_gc (также в листинге 30.2)— это метаметод __gc. Онзакрывает директорию, но при этом он должен соблюдать одну мерупредосторожности: так как мы создаем пользовательские данные дооткрытия директории, эти пользовательские данные должны бытьсобраны сборщиком мусора независимо от результата opendir. Еслиopendirдастсбой,закрыватьбудетнечего.

Последняя функция в листинге 30.2, luaopen_dir, — это функция,котораяоткрываетэтубиблиотекусоднойфункцией.

В этом полном примере есть интересный нюанс. Сперва можетпоказаться, что dir_gc должна проверять, что ее аргумент является

Page 366: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

Листинг30.2.Другиефункциидлябиблиотекиdirstaticintdir_iter(lua_State*L){

DIR*d=*(DIR**)lua_touserdata(L,lua_upvalueindex(1));

structdirent*entry;

if((entry=readdir(d))!=NULL){

lua_pushstring(L,entry->d_name);

return1;

}

elsereturn0;/*большезначенийдлявозвратанет*/

}

staticintdir_gc(lua_State*L){

DIR*d=*(DIR**)lua_touserdata(L,1);

if(d)closedir(d);

return0;

}

staticconststructluaL_Regdirlib[]={

{"open",l_dir},

{NULL,NULL}

};

intluaopen_dir(lua_State*L){

luaL_newmetatable(L,"LuaBook.dir");

/*устанавливаетегополе__gc*/

lua_pushcfunction(L,dir_gc);

lua_setfield(L,-2,"__gc");

/*создаетбиблиотеку*/

luaL_newlib(L,dirlib);

return1;

}

30.2.ПарсерXML

ТеперьмывзглянемнаупрощеннуюреализациюпривязкимеждуLuaи Expat, которую мы назовем lxp. Expat — это парсер XML 1.0 с

Page 367: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

открытым исходным кодом, написанным на С. Он реализует SAX —SimpleAPI forXML. SAX— это событийно-ориентированныйAPI.Этозначит, что парсер SAX читает документ XML и по мере чтениясообщаетприложению,чтооннаходит,припомощиобратныхвызовов.Например,еслимыдадимуказанияExpatразобратьстрокунаподобие"<tag cap="5">hi</tag>", то он сгенерирует три события: событиеначального элемента (start-element), когда он читает подстроку "<tagcap="5">"; событие текста (также называемое событием символьныхданных (character data)), когда он читает "hi"; и событие конечногоэлемента (end-element), когда он читает "</tag>". Каждое из этихсобытийвызываетподходящийобработчикобратных вызовов (callbackhandler)вприложении.

Здесь мы не будем охватывать всю библиотеку Expat. Мысосредоточимся только на тех частях, которые иллюстрируют новыетехникивзаимодействиясLua.ХотяExpatобрабатываетболеедюжиныразличныхсобытий,мырассмотримлишьтетрисобытия,которыемыувидели в предыдущем примере: начальные элементы, конечныеэлементы и текст. (Примечание: Пакет LuaExpat предоставляетпрактическиполныйинтерфейскExpat.)

ДляэтогопримеранампотребуетсялишьмалаячастьExpatAPI.Во-первых,намнужныфункциидлясозданияиуничтоженияпарсераExpat:

XML_ParserXML_ParserCreate(constchar*encoding);

voidXML_ParserFree(XML_Parserp);

Аргументencodingнеобязателен;внашейпривязкемыиспользуемNULL.Послетого,какунасестьпарсер,мыдолжнызарегистрироватьего

обработчикиобратныхвызовов:voidXML_SetElementHandler(XML_Parserp,

XML_StartElementHandlerstart,

XML_EndElementHandlerend);

voidXML_SetCharacterDataHandler(XML_Parserp,

XML_CharacterDataHandlerhndl);

Перваяфункциярегистрируетобработчикидляначальногоиконечногоэлементов. Вторая функция регистрирует обработчики для текста(символьныеданныенаязыкеXML).

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

Page 368: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

typedefvoid(*XML_StartElementHandler)(void*uData,

constchar*name,

constchar**atts);

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

typedefvoid(*XML_EndElementHandler)(void*uData,

constchar*name);

Наконец, обработчик текста получает в качестве дополнительногопараметра только текст. Эта текстовая строка не завершена нуль-символом;вместоэтогоонасодержитявнозаданнуюдлину:

typedefvoid(*XML_CharacterDataHandler)(void*uData,

constchar*s,

intlen);

Для скармливания текста Expat мы воспользуемся следующейфункцией:

intXML_Parse(XML_Parserp,constchar*s,intlen,intisLast);

Expat получает документ, который необходимо разобрать, по частям,через последовательные вызовы XML_Parse. Последний аргументXML_Parse, isLast, сообщает Expat, была ли та часть последней вдокументе. Обратите внимание, что каждой части текста не требуетсязавершаться нуль-символом; вместо этого мы предоставляем явнуюдлину. Функция XML_Parse возвращает нуль, если обнаружит ошибкуразбора. (Expat также предоставляет функции для полученияинформации об ошибке, но для простоты мы не будем их здесьрассматривать.)

Последняя функция, которая нам нужна от Expat, позволяет задатьпользовательскиеданные,которыебудутпереданыобработчикам:

voidXML_SetUserData(XML_Parserp,void*uData);

Теперь давайте посмотрим, как мы можем использовать этубиблиотеку в Lua. Первый подход — это прямой подход: простоэкспортируем все те функции в Lua. Более удачный подход состоит вадаптации этой функциональности к Lua. Например, поскольку Luaявляется языком с динамической типизацией, нам не нужны разные

Page 369: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

функции для каждого вида обратного вызова. Еще лучше то, что мыможем полностью избежать функции для регистрации обратныхвызовов. Вместо этого при создании парсера мы зададим таблицуобратных вызовов, которая содержит все обработчики обратныхвызовов, каждый с подходящим ключом. Например, если нампотребуется напечатать структуру документа, мы можем использоватьследующуютаблицуобратныхвызовов:

localcount=0

callbacks={

StartElement=function(parser,tagname)

io.write("+",string.rep("",count),tagname,"\n")

count=count+1

end,

EndElement=function(parser,tagname)

count=count-1

io.write("-",string.rep("",count),tagname,"\n")

end,

}

Получив на вход строку "<to> <yes/> </to>", эти обработчикинапечаталибыследующийвывод:

+to

+yes

-yes

-to

С этим API нам не нужны функции для управления обратнымивызовами. Мы управляем ими напрямую в таблице обратных вызовов.Такимобразом,длявсегоAPIтребуетсялишьтрифункции:перваядлясоздания парсеров, вторая для обработки части текста и третья длязакрытияпарсера.Насамомделемыреализуемпоследниедвефункциикакметодыобъектовпарсера.ТипичноеприменениеданногоAPIмоглобывыглядетьследующимобразом:

locallxp=require"lxp"

p=lxp.new(callbacks)--создаетновыйпарсер

forlinio.lines()do--итерируетчерезвведенныестроки

assert(p:parse(l))--разбираетэтустроку

assert(p:parse("\n"))--добавляетпереводстроки

end

Page 370: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

assert(p:parse())--завершаетдокумент

p:close()

Теперь давайте обратим наше внимание на реализацию. Первоерешение,котороенужнопринять,—этокакпредставитьпарсервLua.Вполне естественно будет использовать пользовательские данные, ночто нам нужно поместить внутрь них? По меньшей мере нампонадобятсянастоящийпарсерExpat и таблица обратных вызовов.Мыне можем хранить таблицу Lua внутри пользовательских данных (иливнутри какой-либо структуры С), но Lua позволяет каждымпользовательскимданнымиметьпользовательскоезначение (uservalue),которое может быть любой таблицей Lua, связанной с ними.(Примечание:ВLua5.1пользовательским значениемслужитокружениепользовательскихданных.)МытакжедолжныхранитьсостояниеLuaвобъекте парсера, поскольку эти объекты парсера — это все, чтопринимаетобратныйвызовExpat,аобратныевызовынужныдлявызоваLua.Поэтомуопределениеобъектапарсераследующее:

#include<stdlib.h>

#include"expat.h"

#include"lua.h"

#include"lauxlib.h"

typedefstructlxp_userdata{

XML_Parserparser;/*связанныйпарсерexpat*/

lua_State*L;

}lxp_userdata;

Следующимшагомявляетсяфункциядлясозданияобъектовпарсера,lxp_make_parser.Еекодприведенвлистинге30.3.Этафункциясостоитизчетырехважныхшагов:

Еепервыйшагследуетобщепринятомуобразцу:сначалаонасоздаетпользовательскиеданные;затемонапредварительноинициализируетпользовательскиеданныесогласованнымизначениями;и,наконец,онаустанавливаетсвоюметатаблицу.Причинаэтойпредварительнойинициализацииследующая:есливходеинициализациивозникаеткакая-либоошибка,томыдолжныубедиться,чтофинализатор(метаметод__gc)найдетпользовательскиеданныевсогласованномсостоянии.НавторомшагефункциясоздаетпарсерExpat,сохраняетеговпользовательскихданныхипроверяетнаошибки.Третийшагпроверяет,чтопервымаргументомфункции

Page 371: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

действительноявляетсятаблица(таблицаобратныхвызовов),иустанавливаетеекакпользовательскоезначениедляпользовательскихданных.ПоследнийшагинициализируетпарсерExpat.Онустанавливаетпользовательскиеданныекакобъектдляпередачивфункцииобратноговызоваиустанавливаетэтифункцииобратноговызова.Обратитевнимание,чтоэтифункцииобратноговызоваодниитежедлявсехпарсеров;вконцеконцов,наСневозможнодинамическисоздаватьновыефункции.ВместоэтоготефиксированныефункцииCиспользуюттаблицуобратныхвызовов,чтобырешить,какиефункцииLuaимследуетвызвать,когдапридетвремя.

Листинг 30.3. Функция для создания объектов-парсеровXML

/*предваряющиеобъявлениядляфункцийобратноговызова*/

staticvoidf_StartElement(void*ud,

constchar*name,

constchar**atts);

staticvoidf_CharData(void*ud,constchar*s,intlen);

staticvoidf_EndElement(void*ud,constchar*name);

staticintlxp_make_parser(lua_State*L){

XML_Parserp;

/*(1)создаетобъект-парсер*/

lxp_userdata*xpu=(lxp_userdata*)lua_newuserdata(L,

sizeof(lxp_userdata));

/*предварительноинициализируетегонаслучайошибки*/

xpu->parser=NULL;

/*устанавливаетегометатаблицу*/

luaL_getmetatable(L,"Expat");

lua_setmetatable(L,-2);

/*(2)создаетпарсерExpat*/

p=xpu->parser=XML_ParserCreate(NULL);

if(!p)

luaL_error(L,"XML_ParserCreatefailed");

/*(3)проверяетихраниттаблицуобратноговызова*/

luaL_checktype(L,1,LUA_TTABLE);

lua_pushvalue(L,1);/*помещаеттаблицунавершинустека*/

lua_setuservalue(L,-2);/*устанавливаетеекакпользовательское

значение*/

Page 372: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

/*(4)конфигурируетпарсерExpat*/

XML_SetUserData(p,xpu);

XML_SetElementHandler(p,f_StartElement,f_EndElement);

XML_SetCharacterDataHandler(p,f_CharData);

return1;

}

Следующимшагомявляетсяметодпарсераlxp_parse (листинг30.4),который разбирает часть данных XML. Он получает два аргумента:объект парсера (self из этого метода) и необязательную часть данныхXML. При вызове без каких-либо данных он сообщает Expat, что вдокументебольшенеосталосьчастей.

Листинг30.4.ФункциядляразборафрагментаXMLstaticintlxp_parse(lua_State*L){

intstatus;

size_tlen;

constchar*s;

lxp_userdata*xpu;

/*получаетипроверяетпервыйаргумент(долженбытьпарсером)*/

xpu=(lxp_userdata*)luaL_checkudata(L,1,"Expat");

/*проверяет,чтооннезакрыт*/

luaL_argcheck(L,xpu->parser!=NULL,1,"parserisclosed");

/*получаетвторойаргумент(строку)*/

s=luaL_optlstring(L,2,NULL,&len);

/*помещаеттаблицуобратноговызовавстекпоиндексу3*/

lua_settop(L,2);

lua_getuservalue(L,1);

xpu->L=L;/*setLuastate*/

/*вызываетExpatдляразборастроки*/

status=XML_Parse(xpu->parser,s,(int)len,s==NULL);

/*возвращаеткодошибки*/

lua_pushboolean(L,status);

return1;

}

Когда lxp_parse вызывает XML_Parse, последняя функция вызоветобработчикидлякаждогоподходящегоэлемента,которыйонанайдетвданной части документа. Этим обработчикам понадобится доступ ктаблицеобратныхвызовов,поэтомуlxp_parseзаталкиваетэтутаблицув

Page 373: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

стекпоиндексу3 (сразупослепараметров).ЕстьодиннюансввызовеXML_Parse: помните, что последний аргумент этой функции сообщаетExpat, является ли данная часть текста последней. Когда мы вызываемparse без аргументов,s будет равнаNULL, поэтому последний аргументбудетравенtrue.

Теперь давайте обратим наше внимание на функции обратноговызова: f_StartElement, f_EndElement и f_CharData. Все три функцииобладаютодинаковойструктурой:каждаяизнихпроверяет,определяетли таблица обратных вызовов обработчикLua для своего конкретногособытия, и, если определяет, то подготавливает аргументы и затемвызываетэтотобработчикLua.

Давайте сначаларассмотриобработчикf__CharData в листинге 30.5.Его код довольно прост. Обработчик получает структуру lxp_userdataкак свой первый аргумент, поскольку мы вызвали XML_setuserData присоздании парсера. После получения состояния Lua обработчик можетобратитьсяктаблицеобратныхвызововвстекепоиндексу3,заданномуlxp_parse, и к самому парсеру по индексу 1. Затем он вызываетсоответствующий обработчик на Lua (когда он есть) с двумяаргументами:парсеромисимвольнымиданными(строкой).

Листинг30.5.Обработчиксимвольныхданныхstaticvoidf_CharData(void*ud,constchar*s,intlen){

lxp_userdata*xpu=(lxp_userdata*)ud;

lua_State*L=xpu->L;

/*получаетобработчик*/

lua_getfield(L,3,"CharacterData");

if(lua_isnil(L,-1)){/*обработчиканет?*/

lua_pop(L,1);

return;

}

lua_pushvalue(L,1);/*заталкиваетпарсер('self')*/

lua_pushlstring(L,s,len);/*заталкиваетсимвольныеданные*/

lua_call(L,2,0);/*вызываетэтотобработчик*/

}

Обработчикf_EndElementдовольнопохожнаf_CharData;взглянитеналистинг 30.6. Он также вызывает соответствующий обработчик Lua сдвумя аргументами — парсером и именем тега (снова строкой, но наэтотраззавершеннойнуль-символом).

Page 374: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг30.6.Обработчикконечныхэлементовstaticvoidf_EndElement(void*ud,constchar*name){

lxp_userdata*xpu=(lxp_userdata*)ud;

lua_State*L=xpu->L;

lua_getfield(L,3,"EndElement");

if(lua_isnil(L,-1)){/*обработчиканет?*/

lua_pop(L,1);

return;

}

lua_pushvalue(L,1);/*заталкиваетпарсер('self')*/

lua_pushstring(L,name);/*заталкиваетимятега*/

lua_call(L,2,0);/*вызываетэтотобработчик*/

}

Листинг 30.7 показывает последний обработчик, f_startElement. Онвызывает Lua с тремя аргументами: парсером, именем тега и спискоматрибутов.Этотобработчикнемногосложнееостальных,посколькуемунеобходимо перевести список атрибутов тега в Lua. Он используетвполне естественный перевод, строя таблицу, которая связывает именаатрибутовсихзначениями.Напримерначальныйтегнаподобие

<tomethod="post"priority="high">

генерируетследующуютаблицуатрибутов:{method="post",priority="high"}

Листинг30.7.Обработчикначальныхэлементовstaticvoidf_StartElement(void*ud,

constchar*name,

constchar**atts){

lxp_userdata*xpu=(lxp_userdata*)ud;

lua_State*L=xpu->L;

lua_getfield(L,3,"StartElement");

if(lua_isnil(L,-1)){/*обработчиканет?*/

lua_pop(L,1);

return;

}

lua_pushvalue(L,1);/*заталкиваетпарсер('self')*/

lua_pushstring(L,name);/*заталкиваетимятега*/

/*создаетизаполняеттаблицуатрибутов*/

lua_newtable(L);

Page 375: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

for(;*atts;atts+=2){

lua_pushstring(L,*(atts+1));

lua_setfield(L,-2,*atts);/*table[*atts]=*(atts+1)*/

}

lua_call(L,3,0);/*вызываетэтотобработчик*/

}

Последнийметоддляпарсеров—этоclose,показанныйвлистинге30.8. Когда мы закрываем парсер, нам нужно высвободить все егоресурсы, а именно структуру Expat. Помните, что из-за происходящихвременами ошибок при создании у парсера может и не быть этойструктуры. Обратите внимание, как мы поддерживаем парсер всогласованном состоянии по мере его закрытия, так что не будетникаких проблем, если мы попытаемся закрыть его снова, или когдасборщик мусора его финализирует. На самом деле мы будемиспользовать эту функцию именно как финализатор. Это нужно длятого,чтобыкаждыйпарсерсовременемобязательноосвобождалсвоиресурсы,дажееслипрограммистнезакрылего.

Листинг30.8.МетоддлязакрытияпарсераXMLstaticintlxp_close(lua_State*L){

lxp_userdata*xpu=(lxp_userdata*)luaL_checkudata(L,1,

"Expat");

/*бесплатныйпарсерExpat(еслиесть)*/

if(xpu->parser)

XML_ParserFree(xpu->parser);

xpu->parser=NULL;/*предотвращаетегоповторноезакрытие*/

return0;

}

Листинг 30.9 — это завершающий шаг: он показывает функциюluaopen_lxp, которая открывает библиотеку, совмещая в себе все ранеерассмотренные функции. Здесь мы используем ту же схему, что и дляобъектно-ориентированного примера массива логических значений вразделе 29.3: мы создаем метатаблицу, устанавливаем ее точку поля__indexнанеесамуипомещаемвнутрьнеевсеметоды.Дляэтогонампонадобится список с методами парсера (lxp_meths). Также нам нуженсписокфункцийэтойбиблиотеки(lxp_funcs).Какипринятовобъектно-ориентированных библиотеках, этот список содержит всего однуфункцию,котораясоздаетновыепарсеры.

Page 376: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Листинг30.9.КодинициализациидлябиблиотекиlхрstaticconststructluaL_Reglxp_meths[]={

{"parse",lxp_parse},

{"close",lxp_close},

{"__gc",lxp_close},

{NULL,NULL}

};

staticconststructluaL_Reglxp_funcs[]={

{"new",lxp_make_parser},

{NULL,NULL}

};

intluaopen_lxp(lua_State*L){

/*создаетметатаблицу*/

luaL_newmetatable(L,"Expat");

/*metatable.__index=metatable*/

lua_pushvalue(L,-1);

lua_setfield(L,-2,"__index");

/*регистрируетметоды*/

luaL_setfuncs(L,lxp_meths,0);

/*регистрируетфункции(толькоlxp.new)*/

luaL_newlib(L,lxp_funcs);

return1;

}

Упражнения

Упражнение 30.1. Модифицируйте функцию dir_iter в примере сдиректорией так, чтобы она закрывала структуру DIR, когда онадостигаетконцаобхода.Приэтомизменениипрограммененужнождатьсборки мусора для освобождения ресурса, который, как она знает, ейбольшененужен.

(Когда вы закрываете директорию, вы должны выставить адрес,сохраненный в пользовательских данных, на NULL, чтобы сообщитьфинализатору, что директория уже закрыта. Также функция dir_iterперед использованием директории должна проверять, что она незакрыта.)

Упражнение30.2.Впримересlхробработчикначальныхэлементовполучает таблицу с атрибутами элемента. В этой таблице утерянпервоначальныйпорядок,вкотороматрибутыстояливнутриэлемента.

Page 377: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Каквыможетепередатьэтуинформациювобратныйвызов?Упражнение30.3.Впримересlxpмыиспользовалипользовательские

значения, чтобы связать таблицу обработчиков с пользовательскимиданными, представляющими парсер. Этот выбор создал небольшуюпроблему, поскольку то, что получают обратные вызовы С, — этоструктураlxp_userdata,иэтаструктуранепредоставляетпрямойдоступкданнойтаблице.Мырешилиэтупроблемупутемсохранениятаблицыобратных вызовов по индексу стека фиксированного размера во времяразборакаждогофрагмента.

Альтернативным подходом может быть связывание таблицыобратных вызовов с пользовательскими данными посредством ссылок(раздел28.3):мысоздаемссылкунатаблицуобратныхвызововихранимэту ссылку (целое число) в структуре lxp_userdata. Реализуйте данныйподход.Незабудьтевысвободитьссылкупризакрытиипарсера.

Page 378: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА31

Нитиисостояния

Lua не поддерживает настоящую многонитевость, то естьвытесняющиенити,использующиепамятьсовместно.Естьдвепричиныотсутсвияданнойподдержки.Перваяпричинасостоитвтом,чтоANSIСнепредоставляетее,ипоэтомунесуществуетпереносимогоспособареализовать этот механизм в Lua. Второй и более серьезной причинойявляетсято,чтомынесчитаеммногонитевостьхорошейидеейдляLua.

Многонитевость была разработана для низкоуровневогопрограммирования. Механизмы синхронизации вроде семафоров имониторов были разработаны для операционных систем (и опытныхпрограммистов), а не для прикладных программ. Крайне сложнонаходить и исправлять ошибки, связанные с многонитевостыо, инекоторыеизнихмогутпривестикбрешамвбезопасности.Болеетого,многонитевостьможетстатьпричинойснижениябыстродействияиз-занеобходимости синхронизации в ряде критических мест программы,напримерприраспределениипамяти.

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

31.1.Многонитевость

Нить (thread) — это суть сопрограммы в Lua. Мы рассматриваемсопрограмму как нить с удобным интерфейсом, или же мы можемрассматриватьнитькаксопрограммуснизкоуровневымAPI.

СточкизренияС,можетбытьудобнодуматьонитикакостеке—чемнасамомделеиявляетсянитьс точки зренияреализации.Каждыйстек хранит информацию об ожидающих вызовах нити, а такжепараметрах и локальных переменных каждого вызова.Иными словами,

Page 379: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

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

КогдамывызываембольшинствофункцийизLua-CAPI,этифункцииработают с определенным стеком. Например, lua_pushnumber должназаталкивать число в определенный стек; lua_pcall нужен стек вызовов.КакLuaузнает,какойстекследуетиспользовать?Секретзаключаетсявтипеlua_State, первом аргументе этихфункций, которыйпредставляетне только состояние Lua, но и нить внутри этого состояния. (Многиеубеждены,чтоэтоттипдолженназыватьсяlua_Thread.)

Каждый раз, когда вы создаете состояние Lua, Lua автоматическисоздает новую нить внутри этого состояния и возвращает lua_State,представляющее эту нить. Эта главная нить (main thread) никогда неутилизируется. Она высвобождается вместе с состоянием, когда вызакрываетесостояниеприпомощиlua_close.

Вы можете создавать другие нити внутри состояния путем вызоваlua_newthread:

lua_State*lua_newthread(lua_State*L);

Этафункциявозвращаетуказательнаlua_State,представляющееновуюнить,атакжезаталкиваетновуюнитьвстеккакзначениетипа"thread".Например,послевыполненияоператора

L1=lua_newthread(L);

унасбудетдвенити,L1иL,которыебудутвнутреннессылатьсянаоднои то же состояние Lua. Каждая нить обладает собственным стеком.НоваянитьL1начинаетспустогостека;устаройнитиLестьноваянитьнавершинестека:

printf("%d\n",lua_gettop(L1));-->0

printf("%s\n",luaL_typename(L,-1));-->thread

Заисключениемглавнойнити,всенитиподверженысборкемусора,как и любой другой объект Lua. Когда вы создаете новую нить, тозначение, которое заталкивается в стек, гарантирует, что эта нить неявляется мусором. Вы никогда не должны использовать нить, котораядолжным образом не привязана к состоянию. (Главная нить внутреннепривязана с самого начала, поэтому о ней можно не беспокоиться.)Любой вызов Lua API может уничтожить непривязанную нить, дажевызов, использующий эту самую нить. Например, рассмотрим

Page 380: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

следующийфрагмент:lua_State*L1=lua_newthread(L);

lua_pop(L,1);/*L1теперьявляетсямусоромдляLua*/

lua_pushstring(L1,"hello");

Вызовlua_pushstring может запустить сборщикмусора и собрать L1 (ипривести к сбою приложения), несмотря на то, что L1 все ещеиспользуется. Во избежание этого всегда оставляйте ссылку на нити,которые вы используете, например в стеке привязанной нити или вреестре.

Кактолькоунаспоявляетсяноваянить,мыможемиспользоватьеетемже образом, что и главную.Мыможем заталкивать и выталкиватьэлементыизстека,мыможемиспользоватьеедлявызовафункцийит.п.Например, следующийкодвыполняетвызовf(5) в новойнитии затемперемещаетрезультатвстаруюнить:

lua_getglobal(L1,"f");/*допускаетглобальнуюфункцию'f'*/

lua_pushinteger(L1,5);

lua_call(L1,1,1);

lua_xmove(L1,L,1);

Функция lua_xmove перемещает значение Lua между двумя стеками водном и том же состоянии. Вызов наподобие lua_xmove(F,т,n)

выталкиваетnэлементовизстекаFизаталкиваетихвT.Однако,дляэтихцелейнамненужнановаянить;стакимжеуспехом

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

intlua_resume(lua_State*L,lua_State*from,intnarg);

Длязапускавыполнениясопрограммымыиспользуемlua_resumeтакже,какмыиспользуемlua_pcall:мы заталкиваемфункциюдля вызова,заталкиваемееаргументыивызываемlua_resume,передаваявnargчислоаргументов. (Параметр from — это нить, которая совершает вызов.)Данное поведение также очень похоже на lua_pcall, но с тремяотличиями.Во-первых,уlua_resumeнетпараметрадлячислатребуемыхрезультатов;онавсегдавозвращаетвсезначенияизвызваннойфункции.Во-вторых,унеенетпараметрадляобработчикасообщений;ошибканераскручивает стек, поэтому после нее вы можете изучить стек. В-третьих,есливыполняемаяфункцияуступаетуправление,тоlua_resume

Page 381: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

возвращает особый код LUA_YIELD и оставляет нить в состоянии, изкоторогоонаможетбытьвозобновленапозже.

Когда lua_resume возвращает LUA_YIELD, видимая часть стека нитисодержит только значения, переданные yield. Вызов lua_gettop вернетчисло выработанных значений. Для перемещения этих значений вдругуюнитьмыможемиспользоватьlua_xmove.

Чтобы возобновить приостановленную нить, мы вновь вызываемlua_resume. При таких вызовах Lua считает, что все значения в стекедолжныбытьвозвращенывызовомyield.Например,есливынетрогаетестек нити между возвращением из lua_resume и следующимвозобновлением, то yield вернет именно те значения, которые онвыработалприуступкеуправления.

Обычно мы запускаем сопрограмму с функцией Lua в качестве еетела.ЭтафункцияLuaможетвызыватьдругиефункции,илюбаяизэтихфункцийпоройможет уступать управление, завершая вызов lua_resume.Например,допустим,унасестьследующиеопределения:

functionfoo(x)coroutine.yield(10,x)end

functionfoo1(x)foo(x+1);return3end

ТеперьмывыполнимэтоткодC:lua_State*L1=lua_newthread(L);

lua_getglobal(L1,"foo1");

lua_pushinteger(L1,20);

lua_resume(L1,L,1);

Этот вызов lua_resume вернет LUA_YIELD, чтобы сообщить, что нитьуступила управление. В этот момент стек L1 содержит значения,переданныевyield:

printf("%d\n",lua_gettop(L1));-->2

printf("%d\n",lua_tointeger(L1,1));-->10

printf("%d\n",lua_tointeger(L1,2));-->21

Когдамывновьвозобновимэтунить,онапродолжитсястогоместа,где была остановлена (вызовом yield). Отсюда foo вернет управлениеfoo1,аона,всвоюочередь,вернетуправлениеlua_resume:

lua_resume(L1,L,0);

printf("%d\n",lua_gettop(L1));-->1

printf("%d\n",lua_tointeger(L1,1));-->3

Этот второй вызов lua_resume вернет LUA_OK, что означает обычный

Page 382: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

возврат.Сопрограммы также могут вызывать функции С, которые могут

обратно вызывать другие функции Lua. Мы уже обсуждали, какиспользовать продолжения, чтобы позволить этим функциям Luaуступатьуправление(раздел27.2).СамапосебефункцияСтакжеможетуступать управление. В этом случае она также должна предоставитьпродолжающую функцию для вызова при возобновлении нити. ДляуступкиуправленияфункцияCдолжнавызватьследующуюфункцию:

intlua_yieldk(lua_State*L,intnresults,intctx,

lua_CFunctionk);

Мывсегдадолжныиспользоватьэтуфункциювоператоревозврата,как,например,здесь:

staticinfmyCfunction(lua_State*L){

...

returnlua_yieldk(L,nresults,ctx,k);

}

Этот вызов немедленно приостанавливает выполняющуюсясопрограмму. Параметр nresults — это количество значений в стеке,которые нужно вернуть соответствующему lua_resume; ctx — этоконтекстнаяинформация,котораядолжнабытьпереданапродолжению;аk—этопродолжающаяфункция.Когдасопрограммавозобновляется,управление переходит напрямую к продолжающей функции k. Послеуступки управления myCfunction не может больше ничего сделать; онадолжнаделегироватьвсюдальнейшуюработусвоемупродолжению.

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

intprim_read(lua_State*L){

if(nothing_to_read())

returnlua_yieldk(L,0,0,&prim_read);

lua_pushstring(L,read_some_data());

return1;

}

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

Page 383: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

prim_read, поэтому нить снова вызывает prim_read и вновь попытаетсясчитать некоторые данные. (Этот образец функции, вызывающейlua_yieldk, которая является продолжающей функцией, встречаетсядовольночасто.)

Если функции С нечего делать после уступки управления, то онаможетвызватьlua_yieldkбезпродолжающейфункцииилииспользоватьмакросlua_yield:

returnlua_yield(L,nres);

После этого вызова, когда нить снова возобновиться, управлениевернетсякфункции,котораявызывалаmyCfunction.

31.2.СостоянияLua

КаждыйвызовluaL_newstate(илиlua_newstate,какмыувидимвглаве32) создает новое состояние Lua. Разные состояния Lua полностьюнезависимыдруготдруга.Унихвообщенетобщихданных.Этозначит,чтонезависимооттого,чтопроисходитвнутриодногосостоянияLua,оно не может повредить данные другого. Это также значит, чтосостоянияLuaнемогутвзаимодействоватьдругсдругомнапрямую;мыдолжны использовать в качестве посредника какой-нибудь код С.Например, если даны два состояния, L1 и L2, то следующая командазатолкнетвL2строкусвершиныстекавL1:

lua_pushstring(L2,lua_tostring(L1,-1));

Поскольку данные должны быть переданы посредством C, состоянияLua могут обмениваться только типами, которые могут бытьпредставлены в C, например, строками и числами. Другие типы, такиекактаблицы,должныбытьсериализованыдляосуществленияпереноса.

В системах, которые предлагают многонитевость, интереснойсхемой построения является создание независимого состояния Lua длякаждой нити. Эта схема в результате дает нити, похожие на процессыUNIX, где мы получаем согласованность без общей памяти. В этомразделе мы разработаем прототипную реализацию многонитевости,следуяданномуподходу.ДляэтойреализацииябудуиспользоватьнитиPOSIX (pthreads). При портировании данного кода на другие нитевыесистемынедолжновозникнутьсложностей,таккаквнемиспользуютсялишьбазовыесредства.

Page 384: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Система, которую мы собираемся построить, очень проста. ЕеосновнымпредназначениемявляетсяпоказатьиспользованиенесколькихсостоянийLuaвконтекстемногонитевости.Послетого,каконабудетготова, вы можете реализовать на ее основе некоторые продвинутыевозможности.Мыназовемнашубиблиотекуlproc.Онапредлагаетвсегочетырефункции:

lproc.start(chunk)запускаетновыйпроцессдлявыполнениязаданногокуска(строки).ДаннаябиблиотекареализуетпроцессLuaкакнитьСплюссвязанноеснейсостояниеLua.lproc.send(channel,val1,val2,...)отправляетвсезаданныезначения(которыедолжныбытьстроками)заданномуканалу(которыйидентифицируютпоегоимени,тожестроке).lproc.receive(channel)получаетзначения,отправленныезаданномуканалу.lproc.exit()завершаетпроцесс.Этафункциятребуетсятолькодляглавногопроцесса.Еслиэтотпроцессзавершаетсябезвызоваlproc.exit,товсяпрограммапрекращаетсвоевыполнениебезожиданиязавершениядругихпроцессов.

Библиотекаидентифицируетканалыприпомощистрокииспользуетихдля сопоставления отправителей и получателей. Операция отправкиможет отправить любое количество строковых значений, которыевозвращаются сопоставленной с ней операцией получения. Всевзаимодействие синхронно: процесс, отправляющий сообщение каналу,блокируется до тех пор, пока есть процесс, принимающий из этогоканала,втовремякакпроцесс,получающийизканала,блокируетсядотехпор,покаестьпроцесс,отправляющийему.

Реализация lproc также проста, как и ее интерфейс.Она используетдва кольцевых двунаправленных списка, один для процессов,ожидающихотправкисообщения,адругойдляпроцессов,ожидающихприемасообщения.Онаиспользуетодинединственныймьютекс(объектвзаимоисключающей блокировки) для управления доступом к этимспискам.Укаждогопроцессаестьсвязаннаяснимусловнаяпеременная.Когда процесс хочет отправить каналу сообщение, он обходит списокполучателей в поисках процесса, ожидающего этот канал. Если поискуспешен, то отправляющий процесс убирает найденный процесс изсписка ожидания, перемещает из себя в него значения сообщения ипредупреждает об этом другой процесс. В противном случае он

Page 385: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

вставляет себя в список отправителей и ожидает своей условнойпеременной.Получениесообщенияпроисходитсимметричнымобразом.

Главным элементом данной реализации является структура,представляющаяпроцесс:

#include<pthread.h>

#include"lua.h"

typedefstructProc{

lua_State*L;

pthread_tthread;

pthread_cond_tcond;

constchar*channel;

structProc*previous,*next;

}Proc;

Первые два поля представляют из себя состояние Lua, используемоепроцессом, и нить С, выполняющую данный процесс. Другие поляиспользуются лишь тогда, когда процесс должен ждать подходящегоотправителя или получателя. Третье поле, cond, — это условнаяпеременная, которую нить использует для блокировки самой себя;четвертое поле хранит ожидаемый процессом канал; последние дваполя,previousиnext,используютсядлясвязыванияструктурыпроцессавспискеожидания.

Следующийкодобъявляетдваспискаожиданияисвязанныйснимимьютекс:

staticProc*waitsend=NULL;

staticProc*waitreceive=NULL;

staticpthread_mutex_tkernel_access=PTHREAD_MUTEX_INITIALIZER;

КаждомупроцессунужнаструктураProcидоступкэтойструктуревсякий раз, когда скрипт вызывает send или receive. Единственныйпараметр, который принимают эти функции— это состояние Lua дляпроцесса. Таким образом, каждый процесс должен хранить своюструктуруProcвнутрисвоегосостоянияLua.Внашейреализациикаждоесостояние хранит свою соответственную структуру Proc в реестре какполные пользовательские данные, связанные с ключом "_SELF".ВспомогательнаяфункцияgetselfвозвращаетструктуруProc,связаннуюсданнымсостоянием:

staticProc*getself(lua_State*L){

Proc*p;

lua_getfield(L,LUA_REGISTRYINDEX,"_SELF");

Page 386: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

p=(Proc*)lua_touserdata(L,-1);

lua_pop(L,1);

returnp;

}

Следующая фyнкция, movevalues, перемещает знaчeния изотправляющегопроцессавпринимающий:

staticvoidmovevalues(lua_State*send,lua_State*rec){

intn=lua_gettop(send);

inti;

for(i=2;i<=n;i++)/*переноситзначениявполучателя*/

lua_pushstring(rec,lua_tostring(send,i));

}

Онаперемещаетвполучателявсезначенияизстекаотправителя,кромесамогопервого,которымявляетсяканал.

Листинг 31.1 определяет функцию searchmatch, которая обходитсписок ожидания в поисках процесса, ожидающего заданный канал.Если функция находит такой канал, то она удаляет его из списка ивозвращаетего;иначеонавозвращаетNULL.

Листинг31.1.Функциядляпоискапроцесса, ожидающегозаданногоканала

staticProc*searchmatch(constchar*channel,Proc**list){

Proc*node=*list;

if(node==NULL)returnNULL;/*пустойсписок*/

do{

if(strcmp(channel,node->channel)==0){/*совпадение?*/

/*удаляетузелизэтогосписка*/

if(*list==node)/*этотузелявляетсяпервымэлементом?*/

*list=(node->next==node)?NULL:node->next;

node->previous->next=node->next;

node->next->previous=node->previous;

returnnode;

}

node=node->next;

}while(node!=*list);

returnNULL;/*совпаденийнет*/

}

Последняявспомогательнаяфункция,определеннаявлистинге31.2,вызывается,когдапроцесснеможетнайтисоответствие.Вэтомслучаепроцесс ставит себя в конец соответствующего списка ожидания иожидает,покаснимнебудетсопоставлендругойпроцесс,которыйегои разбудит. (Цикл вокруг pthread_cond_wait защищает от случайных

Page 387: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

пробуждений, которые возможны в нитях POSIX.) Когда один процессбудит другой, он устанавливает поле channel другого процесса в NULL.Поэтому, если p->channel не равен NULL, это означает, что ни одинпроцесснеподошелпроцессуp,ипоэтомуондолженждатьдальше.

Листинг31.2.Функциядлядобавленияпроцессавсписокожидания

staticvoidwaitonlist(lua_State*L,constchar*channel,

Proc**list){

Proc*p=getself(L);

/*linkitselfattheendofthelist*/

if(*list==NULL){/*emptylist?*/

*list=p;

p->previous=p->next=p;

}

else{

p->previous=(*list)->previous;

p->next=*list;

p->previous->next=p->next->previous=p;

}

p->channel=channel;

do{/*waitsonitsconditionvariable*/

pthread_cond_wait(&p->cond,&kernel_access);

}while(p->channel);

}

Имея эти вспомогательные функции, мы можем написать send иreceieve(листинг31.3).Функцияsendначинаетспроверкиканала.Затемона блокируетмьютекс и ищет подходящего получателя. Если она егонаходит,тоонаперемещаетсвоизначениявэтогополучателя,помечаетполучателякакготовогоквыполнениюибудитего.Впротивномслучаеона ставит себя в режим ожидания.По завершении этой операции онаразблокирует мьютекс и возвращается без значений в Lua. Функцияreceiveаналогична,ноонадолжнавернутьвсеполученныезначения.

Листинг 31.3. Функции для отправки и получениясообщений

staticintll_send(lua_State*L){

Proc*p;

constchar*channel=luaL_checkstring(L,1);

Page 388: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

pthread_mutex_lock(&kernel_access);

p=searchmatch(channel,&waitreceive);

if(p){/*найденподходящийресивер?*/

movevalues(L,p->L);/*перемещаетзначениявполучателя*/

p->channel=NULL;/*помечаетполучателякакнеожидающего

*/

pthread_cond_signal(&p->cond);/*будитего*/

}

else

waitonlist(L,channel,&waitsend);

pthread_mutex_unlock(&kernel_access);

return0;

}

staticintll_receive(lua_State*L){

Proc*p;

constchar*channel=luaL_checkstring(L,1);

lua_settop(L,1);

pthread_mutex_lock(&kernel_access);

p=searchmatch(channel,&waitsend);

if(p){/*найденподходящийотправитель?*/

movevalues(p->L,L);/*получаетзначенияототправителя*/

p->channel=NULL;/*помечаетотправителякакне

ожидающего*/

pthread_cond_signal(&p->cond);/*будитего*/

}

else

waitonlist(L,channel,&waitreceive);

pthread_mutex_unlock(&kernel_access);

/*возвращаетвсезначенияизстека,кромеканала*/

returnlua_gettop(L)-1;

}

Теперь давайте посмотрим, как создавать новые процессы.Новомупроцессу нужна новая нить POSIX, а новой нити нужно тело длявыполнения.Мыопределимэтотелопозже;нижеприведенеепрототипнаосновеpthreads:

staticvoid*ll_thread(void*arg);

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

Page 389: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

выполняет первые три задачи, а новая нить делает остальное. (Дляупрощения обработки ошибок система начинает новую нить толькопослетого,каконауспешноскомпилируетпереданныйкусок.)

Функцияll_startсоздаетновыйпроцесс(листинг31.4).Этафункциясоздает новое состояниеLua L1 и компилирует заданный кусок в этомновомсостоянии.ВслучаеошибкифункциясообщаетонейисходномусостояниюL.Затемонасоздаетновуюнить(припомощиpthread_create)с телом ll_thread, передавая новое состояние L1 как аргумент тела.Вызов pthread_detach сообщает системе, что мы не ожидаем никакогоокончательногоответаотэтойнити.

Листинг31.4.Функциядлясозданияновыхпроцессовstaticintll_start(lua_State*L){

pthread_tthread;

constchar*chunk=luaL_checkstring(L,1);

lua_State*L1=luaL_newstate();

if(L1==NULL)

luaL_error(L,"unabletocreatenewstate");

if(luaL_loadstring(L1,chunk)!=0)

luaL_error(L,"errorstartingthread:%s",

lua_tostring(L1,-1));

if(pthread_create(&thread,NULL,ll_thread,L1)!=0)

luaL_error(L,"unabletocreatenewthread");

pthread_detach(thread);

return0;

}

Теломкаждойновойнитиявляетсяфункцияll_thread(листинг31.5).ОнаполучаетсвоесоответственноесостояниеLua(созданноеll_start)сужескомпилированнымглавнымкускомвстеке.Новаянитьоткрываетстандартные библиотеки Lua, открывает библиотеку lproc и затемвызываетсвойглавныйкусок.Вконцеонауничтожаетсвоюусловнуюпеременную (которая была создана luaopen_lproc) и закрывает своесостояниеLua.

Листинг31.5.Телодляновыхнитейintluaopen_lproc(lua_State*L);

staticvoid*ll_thread(void*arg){

Page 390: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

lua_State*L=(lua_State*)arg;

luaL_openlibs(L);/*openstandardlibraries*/

luaL_requiref(L,"lproc",luaopen_lproc,1);

lua_pop(L,1);

if(lua_pcall(L,0,0,0)!=0)/*callmainchunk*/

fprintf(stderr,"threaderror:%s",lua_tostring(L,-1));

pthread_cond_destroy(&getself(L)->cond);

lua_close(L);

returnNULL;

}

(Обратите внимание на использование luaL_requiref для открытиябиблиотеки lproc (Примечание: Эта функция появилась в Lua 5.2). Этафункция в чем-то эквивалентна require, но вместо поиска загрузчикаиспользует заданную функцию (в нашем случае luaopen_lproc) дляоткрытия библиотеки. После вызова этой открывающей функции,luaL_requiref регистрирует результат в таблице package.loaded, чтобыприбудущихвызовахrequireдлябиблиотекионанепыталасьоткрытьее снова. Если последним параметром является true, то она такжерегистрируетэтубиблиотекувсоответственнойглобальнойпеременной(внашемслучаеlproc).)

Последняяфункциявнашеммодуле,exit,довольнопроста:staticintll_exit(lua_State*L){

pthread_exit(NULL);

return0;

}

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

Нашим последний шагом является определение открывающейфункции для модуля lproc. Открывающая функция luaopen_lproc

(листинг 31.6) должна, как обычно, зарегистрировать функциимодуля,но также она должна создать и инициализировать структуру Ргос

выполняемогопроцесса.

Листинг31.6.ОткрывающаяфункциядлямодуляlprocstaticconststructluaL_regll_funcs[]={

{"start",ll_start},

{"send",ll_send},

{"receive",ll_receive},

{"exit",ll_exit},

{NULL,NULL}

};

Page 391: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

intluaopen_lproc(lua_State*L){

/*создаетсобственныйблокуправления*/

Proc*self=(Proc*)lua_newuserdata(L,sizeof(Proc));

lua_setfield(L,LUA_REGISTRYINDEX,"_SELF");

self->L=L;

self->thread=pthread_self();

self->channel=NULL;

pthread_cond_init(&self->cond,NULL);

luaL_register(L,"lproc",ll_funcs);/*открываетбиблиотеку*/

return1;

}

Какясказалранее,даннаяреализацияпроцессоввLuaоченьпростая.Есть бесконечное число улучшений, которые вы можете произвести.Здесьякраткорасскажуонекоторыхизних.

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

Другое улучшение относится к эффективности создания процесса.Создание нового состояния Lua — это быстрая операция. Однако,открытиевсехстандартныхбиблиотекпроисходитуженетакбыстро,абольшинствупроцессов,скореевсего,непонадобятсявсестандартныебиблиотеки. Мы можем избежать затрат на открытие библиотеки припомощи предварительной регистрации библиотек, которую мыобсуждали в разделе 15.1. При данном подходе вместо вызоваluaL_requiref длякаждойстандартнойбиблиотекимылишьпомещаемфункцию, открывающую библиотеку, в таблицу package.preload. Еслипроцесс вызоветrequire"lib", то тогдаи только тогдаrequire вызоветсвязанную с ней функцию для открытия библиотеки. Функцияregisterlib,влистинге31.7,проводитэтурегистрацию.

Листинг 31.7. Регистрация библиотек для открытия позапросу

staticvoidregisterlib(lua_State*L,constchar*name,

lua_CFunctionf){

lua_getglobal(L,"package");

lua_getfield(L,-1,"preload");/*получает'package.preload'*/

lua_pushcfunction(L,f);

lua_setfield(L,-2,name);/*package.preload[name]=f*/

lua_pop(L,2);/*выталкиваеттаблицы'package'и'preload'*/

}

Page 392: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

staticvoidopenlibs(lua_State*L){

luaL_requiref(L,"_G",luaopen_base,1);

luaL_requiref(L,"package",luaopen_package,1);

lua_pop(L,2);/*удаляетрезультатыизпредыдущихвызовов*/

registerlib(L,"io",luaopen_io);

registerlib(L,"os",luaopen_os);

registerlib(L,"table",luaopen_table);

registerlib(L,"string",luaopen_string);

registerlib(L,"math",luaopen_math);

registerlib(L,"debug",luaopen_debug);

}

Открыть основную библиотеку — это всегда хорошая идея. Вамтакже понадобится библиотека для пакетов; иначе require не будетдоступнадляоткрытиядругихбиблиотек.(Увасдаженебудеттаблицыpackage.preload.) Все другие библиотеки могут быть необязательными.Поэтому вместо вызова luaL_openlibs мы можем вызвать нашусобственнуюфункциюopenlibs(такжепоказаннуювлистинге31.7)приоткрытииновыхсостояний.Всякийраз,когдапроцессутребуетсяоднанаэтихбиблиотек,онзапрашиваетееявнымобразом,иrequireвызоветсоответствующуюфункциюlunopen_*.

Другие улучшения включают в себя примитивныекоммуникационныефункции.Например,былобыудобнозадаватьлимитвремени для lproc.send и lproc.receive, определяющий, сколько онидолжны ждать совпадения. В частности, нулевой лимит делал бы этифункции неблокирующими. В нитях POSIX мы можем реализоватьданныесредстваприпомощиpthread_cond_timedwait.

Упражнения

Упражнение31.1. Какмы видели, еслифункция вызывает lua_yield(версию без продолжающей функции), то управление возвращаетсяфункции, которая ее вызвала, когда нить была вновь возобновлена.Какие значения вызывающая функция получит в качестве результатовтоговызова?

Упражнение31.2.Измените библиотекуlproc так, чтобы она моглаотправлять и принимать другие примитивные типы, такие каклогические значения и числа. (Подсказка: вам нужно изменить лишьфункциюmovevalues.)

Упражнение 31.3. Реализуйте в библиотеке lproc неблокирующую

Page 393: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

операциюsend.

Page 394: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ГЛАВА32

Управлениепамятью

Lua выделяет все свои структуры данных динамически. Все этиструктурырастутпомеренадобностиисовременемсокращаютсяилиисчезают.

Lua строго следит за своим использованием памяти. Когда мызакрываем состояние Lua, то Lua явно освобождает всю его память.Более того, все объекты внутри Lua подвержены сборке мусора: нетолькотаблицыистроки,нотакжефункции,нитиимодули(посколькунасамомделеониявляютсятаблицами).

Способ, которымLua управляет памятью, удобен для большинстваприложений. Однако, для некоторых особых приложений можетпотребоваться адаптация, например для работы в условияхограниченногообъемапамятиилидляуменьшенияпаузмеждусборкоймусорадоминимума.Luaпозволяетосуществлятьподобныеадаптациина двух уровнях. На нижнем уровне мы можем задать функциювыделения памяти, используемую Lua. На более высоком уровне мыможемзадатьнекоторыепараметрыдляуправлениясборщикоммусораилиможемдажеполучитьнаднимпрямойконтроль.Вданнойглавемырассмотримэтисредства.

32.1.Выделяющаяфункция

ЯдроLuaнестроитпредположенийотом,каквыделитьпамять.Длявыделенияпамятиононевызываетниmalloc,ниrealloc.Вместоэтогооно осуществляет все свое выделение и освобождение памяти черезодну единственнуювыделяющуюфункцию (allocation function), которуюпользовательдолженпредоставитьприсозданиисостоянияLua.

Функция luaL_newstate, которую мы использовали для созданиясостояний, является вспомогательной функцией, которая создаетсостояниеLuaсвыделяющейфункциейпоумолчанию.Этафункцияпоумолчанию использует стандартные функции malloc-realloc-free изстандартной библиотеки С, которых (должно быть) достаточно дляобычныхприложений.Однако,можнодовольнолегкополучитьполныйконтроль над выделением памяти в Lua, создав ваше состояние при

Page 395: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

помощипримитивнойфункцииlua_newstate:lua_State*lua_newstate(lua_Allocf,void*ud);

Эта функция получает два аргумента: выделяющую функцию ипользовательские данные. Состояние, созданное таким образом,осуществляет все выделение и освобождение памяти при помощивызовов функции f. (Даже сама структура lua_state выделяется припомощиf.)

Типвыделяющейфункцииlua_Aliocопределенследующимобразом:typedefvoid*(*lua_Alloc)(void*ud,

void*ptr,

size_tosize,

size_tnsize);

Первый параметр— это всегда пользовательские данные, которые мыпредоставили lua_newstate; второй параметр — это адрес блока,которыймы хотим заново выделить или освободить; третий параметр— это исходный размер этого блока; и четвертый параметр — этозапрашиваемыйразмерблока.

Luaгарантирует,чтоеслиptrнеравенNULL,тоонбылранеевыделенсразмеромosize.

LuaиспользуетNULLдляблоковнулевогоразмера.Когдаnsize равеннулю, выделяющая функция должна освободить блок, на которыйуказывает ptr, и вернуть NULL, который соответствует блокузапрошенногоразмера(нулевого).КогдаptrравенNULL,функциядолжнавыделить и вернуть блок заданного размера; если она не можетвыделить блок заданного размера, то она должна вернуть NULL. Если иptr равен NULL, и nsize равен нулю, то функция ничего не делает ивозвращаетNULL.

Наконец, когда иptr не равенNULL, иnsize не равен нулю, функциядолжна заново выделить этот блок, как это делает realloc, и вернутьновый адрес (который может как совпадать, так и отличаться отисходного). Опять же, в случае ошибки она должна вернуть NULL. Luaпредполагает,чтовыделяющаяфункцияникогданедаетсбоеввслучае,когда новый размер меньше или равен старому размеру. (Lua сжимаетнекоторыеструктурывовремясборкимусораипотомунеспособенприэтомвосстановитьсяпослеошибок.)

Стандартная выделяющая функция, используемая luaL_newstate,имеетследующееопределение(извлеченонапрямуюизфайлаlauxlib.с):

Page 396: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

void*l_alloc(void*ud,void*ptr,size_tosize,size_tnsize){

if(nsize==0){

free(ptr);

returnNULL;

}

else

returnrealloc(ptr,nsize);

}

Она допускает, что free(NULL) не делает ничего, и что вызовrealloc(NULL,size) эквивалентен malloc(size). Стандарт ANSI Cподдерживаетобаэтихрежима.

Вы можете получить выделяющую функцию из состояния Luaпосредствомвызоваlua_getallocf:

lua_Alloclua_getallocf(lua_State*L,void**ud);

Если ud не равен NULL, to функция установит *ud в значенияпользовательских данных, используемых для данной выделяющейфункции. Вы можете изменить выделяющую функцию для состоянияLuaприпомощивызоваlua_setallocf:

voidlua_setallocf(lua_State*L,lua_Allocf,void*ud);

Имейте в виду, что любая новая выделяющая функция будетответственна за освобождение блоков, выделенных предыдущейфункцией. Чаще всего новая функция является оберткой над старой,например, для отслеживания выделений или синхронизации доступа ккуче.

Внутри себя Lua не кэширует свободные блоки для их повторногоиспользования. Она предполагает, что кэширование проводитвыделяющаяфункция;хорошиевыделяющиефункциитакиделают.Luaне пытается минимизировать фрагментацию памяти. Исследованияпоказывают, что фрагментация— это больше результат плохой схемывыделения памяти, чем поведения программы; хорошие выделяющиефункциинесоздаютсильнойфрагментации.

Сложноулучшитьхорошореализованнуювыделяющуюфункцию,ноиногда вы можете попытаться. Например, Lua дает вам старый размерлюбого блока при его освобождении или выделении заново.Соответственно, специализированной выделяющей функции не нужнохранить информацию о размере блока, тем самым снижая объемтребуемойпамятиподкаждыйблок.

Другойслучай,когдавыможетеулучшитьвыделениепамяти—это

Page 397: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

случай многонитевых систем. Такие системы обычно требуютсинхронизациидлясвоихвыделяющихфункций,таккаконииспользуютглобальный ресурс (память). Однако, доступ к состоянию Lua такжедолженбытьсинхронизирован—или,чтоещелучше,ограниченвсегоодной нитью, как в нашей реализации lproc в главе 31. Поэтому есликаждое состояние Lua будет выделять память из закрытого пула,выделяющая функция может избежать расходов на дополнительнуюсинхронизацию.

32.2.Сборщикмусора

До версии 5.0 Lua использовал простой сборщик мусора типа«пометь и почисти» (mark-and-sweep), также называемый сборщикомтипа«остановимир»(stop-the-world).Этозначит,чтовремяотвремениLua прекращает интерпретировать главнуюпрограмму для выполненияполногоцикласборкимусора.Каждыйтакойциклсостоитизтрехфаз:пометка(mark),уборка(cleaning)иочистка(sweep).

Lua начинает фазу пометки с того, что помечает как живой свойкорневой набор (root set), который включает в себя все объекты, ккоторым Lua имеет прямой доступ: реестр и главная нить. Любойобъект, который хранится в живом объекте, доступен программе, ипоэтомутожепомечаетсякакживой.Фазапометкизаканчивается,когдавседоступныеобъектыпомеченыкакживые.

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

Фаза очистки обходит все объекты Lua. (Чтобы данный обход былвозможен, Lua хранит все создаваемые объекты с связанном списке.)Если объект не помечен как живой, Lua собирает его как мусор. Впротивном случае Lua снимает его пометку для подготовки кследующему циклу. Во время фазы очистки Lua также вызываетфинализаторыобъектов,которыебылиотделенывовремяфазыуборки.

С версией 5.1 Lua получил инкрементальный сборщик мусора. Этотсборщиквыполняеттежешаги,чтоипрежний,нововремявыполненияему не нужно «останавливать мир». Вместо этого он выполняется

Page 398: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

вместе с интерпретатором. Каждый раз, когда интерпретатор выделяетнекоторое количество памяти, сборщик мусора выполняет небольшойшаг. Это значит, что во время работы сборщика интерпретатор можетизменить доступность объекта. Чтобы обеспечить правильностьработысборщиканекоторыеоперациивинтерпретатореимеютбарьеры,которые обнаруживают опасные изменения и поправляют пометкивовлеченныхобъектов.

APIсборщикамусора

Lua предлагает API, который позволяет получить некоторыйконтрольнадсборщикоммусора.ИзСмыможемвызватьlua_gc:

intlua_gc(lua_State*L,intwhat,intdata);

ИзLuaмыиспользуемфункциюcollectgarbage:collectgarbage(what[,data])

Обефункциипредоставляютоднуитужефункциональность.Аргументwhat(перечислимоезначениевС,строкавLua)определяет,чтотребуетсяделать.Возможныеварианты:

LUA_CGSTOP("stop"):останавливаетсборщикмусорадодругоговызоваcollectgarbage(илидоlua_gc)сопцией"restart".LUA_GSRESTART("restart"):перезапускаетсборщикмусора.LUA_GCCOLLECT("collect"):осуществляетполныйциклсборкимусора,приэтомвсенедоступныеобъектысобираютсяифинализируются.Этозначениепоумолчаниюдляcollectgarbage.LUA_GCSTEP("step"):выполняетнекоторуюработупосборкемусора.Объемэтойработыэквивалентентому,чтосборщикмусорасделаетпослевыделенияdataбайт.LUA_GCCOUNT("count"):возвращаетколичествокилобайтпамяти,используемойLuaвданныймомент.Этоколичествовключаетвсебя«мертвые»,ноещенесобранныеобъекты.LUA_GCCOUNTB(безаргументов):возвращаетдробнуючастьчислакилобайтпамяти,используемойLuaвданныймомент.ВCследующеевыражениевозвращаетполныйобъемпамятивбайтах(полагая,чтоэточислопоместитсявint):

(lua_gc(L,LUA_GCCOUNT,0)*1024)

Page 399: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

+lua_gc(L,LUA_GCCOUNTB,0)

В Lua результат collectgarbage ("count") — это число сплавающей точкой, и полное число байт памяти может бытьвычисленоследующимобразом:

collectgarbage("count")*1024

Поэтомууcollectgarbageнетаналогадляэтойопции.LUA_GCSETPAUSE("setpause"):задаетпараметрpauseсборщикамусора.Этозначениезадаетсяпараметромdataвпроцентах:когдаdataравен100,параметрустанавливаетсянаединицу(100%).LUA_GCSETSTEPMUL("setstepmul"):задаетпараметрпошаговогомножителя(stepmul)длясборщикамусора.Этазначениетакжезаданоdataвпроцентах.

Любой сборщик мусора расплачивается за память процессорнымвременем. В одном крайнем случае сборщик может вообще незапуститься.Онсовсемнебудетрасходоватьпроцессорноевремязасчетогромного потребления памяти. В другом крайнем случае сборщикможетпроизводитьполныйциклсборкимусораприкаждомизмененииграфа доступности. Такая программа будет использовать самыйминимум необходимой ей памяти ценой гигантского потребленияпроцессорного времени. Настоящие сборщики мусора пытаются найтихорошийбалансмеждуэтимидвумякрайностями.

Как и ее выделяющая функция, сборщик мусора Lua достаточнохорошдлябольшинстваприложений.Темнеменее,внекоторыхслучаяхсборщикмусорастоитпопробоватьоптимизировать.Двапараметра,—pause и stepmul, предоставляют некоторое управление поведениемсборщика.

Параметр pause управляет тем, как долго сборщик ждет междуокончаниемпоследнейсборкимусораиначаломновой.Значение pauseравное нулю заставит Lua запустить новую сборку мусора сразу позавершении предыдущей. Параметр pause в 200% ожидает удвоенияиспользованияпамятипередтем,какначатьсборкумусора.Выможетепонизить значение pause, если хотите пожертвовать процессорнымвременем ради меньшего потребления памяти. Обычно вы должныдержатьэтозначениемежду0и200%.

Параметрstepmul управляет тем, какмногоработысборщикмусоравыполняет для каждого килобайта выделенной памяти. Чем выше это

Page 400: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

значение, тем менее инкрементальным становится сборщик мусора.Огромноезначениевроде100000000%заставитсборщикмусоравестисебя как неинкрементальный сборщик. Значением по умолчаниюявляется 200%. Значения, меньшие 100%, сделают сборщик настолькомедленным,чтоонможеттакникогдаинезавершитьсборкумусора.

Другиеопцииlua_gc даютвамконтрольнад тем, когда запускаетсясборщик мусора. Игры являются типичными клиентами для данноговида контроля. Например, если вы не хотите, чтобы сборка мусоравыполнялась во время определенных периодов времени, вы можетеостановить его при помощи collectgarbage("stop") и затем запуститьсноваприпомощиcollectgarbage("restart").Всистемах,гдевозникаютпостоянные периоды ожидания, вы можете держать сборщик мусораостановленным и вызывать collectgarbage("step",n) лишь в этипериоды. Чтобы определить, как много работы нужно выполнить вовремя такого периода, либо экспериментально подберите подходящеезначениедляn,либовциклевызывайтеcollectgarbageсn,равнымнулю,досамогоистеченияэтогопериодаожидания.

Упражнения

Упражнение 32.1. Напишите библиотеку, которая позволит скриптуограничитьобщийобъемпамяти,используемыйсостояниемвLua.Онаможет предлагать единственную функцию setlimit для задания этогоограничения.

Библиотека должна задавать свою собственную выделяющуюфункцию.Этафункцияперед вызовомисходной выделяющейфункциипроверяет общий объем используемой памяти и возвращает NULL, еслизапрашиваемыйобъемпревыситмаксимальноезначение.

(Подсказка: эта библиотека может использовать lua_gc дляинициализации своего счетчика памяти при запуске. Она также можетиспользовать пользовательские данные выделяющей функции, чтобыхранить свое состояние: число байт, текущее ограничение объемапамяти и т. п. Не забудьте использовать исходные пользовательскиеданныепривызовеисходнойвыделяющейфункции.)

Упражнение 32.2. Для этого упражнения вам понадобится какминимумодин скриптLua, использующийоченьмногопамяти.Если увас такогонет, тонапишитеего. (Онможетбытьпростым,наподобиецикла,создающеготаблицы.)

Page 401: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Запуститевашскриптсразличнымипараметрамисборщикамусора.Насколькоонивлияютнабыстродействие?Чтослучится,есливыустановитепараметрpauseнаноль?Чтослучится,есливыустановитеегона1000?Чтослучится,есливыустановитепараметрstepmulнаноль?Чтослучится,есливыустановитеегона1000000?Поправьтевашскрипттакимобразом,чтобыонполучилполныйконтрольнадсборщикоммусора.Ондолжендержатьсборщикмусораостановленнымивызыватьеговремяотвремени.Можетеливыповыситьбыстродействиевашегоскриптасданнымподходом?

Page 402: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Приложения

Lua5.3

Page 403: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ПРИЛОЖЕНИЕА

ПереходнаLua5.3

А.1.Изменениявязыке

Главным отличием между Lua 5.2 и Lua 5.3 является введениеподтипа целых чисел — integer. Хотя данное изменение не должноповлиять на «обычные» вычисления, некоторые из них (главнымобразом те, что содержат какое-либо переполнение) могут выдаватьдругиерезультаты.

Вы можете убрать влияние этих отличий путем принудительноговыставления подтипа чисел с плавающей точкой float (в Lua 5.2 всечисла были с плавающей точкой), например, записав константы сокончанием.0 или воспользовавшись x = x + 0.0 для преобразованияпеременной. (Данная рекомендация служит лишь для быстрогоисправления редко встречающейся несовместимости подтипов; она неявляется нормой в хорошем программировании. Для хорошегопрограммирования используйте float там, где вам нужны числа сплавающейточкой,иintegerтам,гдевамнужныцелыечисла.

Преобразование числа с плавающей точкой в строку теперьдобавляет суффикс .0 к результату, если этот результат выглядит какцелоечисло.(Например,число2.0будетнапечатанокак2.0,анекак2.)Вывсегдадолжныявноуказыватьформат,когдавамнуженконкретныйформатдлячисел.

(Формально это не является изменением, поскольку Lua неопределяет, каким образом числа форматируются как строки, нонекоторыепрограммыожидаютконкретныйформат.)

Из сборщика мусора был убран накопительный режим (generationalmode).(ОнбылэкспериментальнойвозможностьювLua5.2.)

А.2.Изменениявбиблиотеках

Библиотека bit32 была исключена. Несложно подобратьсовместимую внешнюю библиотеку или, что еще лучше, заменить еефункции соответственными побитовыми операциями. (Имейте в виду,

Page 404: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

чтоbit32оперирует32-битнымицелымичислами,т.е.частьюfloat,втовремя как побитовые операции в Lua 5.3 оперируют целыми числамиLua,укоторыхпоумолчанию64бит.)

Библиотека table теперь распознает метаметоды для установки иполученияэлементов.

Итератор ipairs теперь распознает метаметоды, а его метаметод__ipairsбылисключен.

Необязательнымименамвio.readбольшененужноначинатьсяс '*'.Для совместимости Lua продолжить принимать (и игнорировать)данныйсимвол.

Следующие функции были исключены из математическойбиблиотеки:atan2,cosh,sinh,tanh,pow,frexpиldexp.Выможетезаменитьmath.pow(x,y) на x^y; вы можете заменить math.atan2 на math.atan,которыйтеперьпринимаетодинилидвапараметра;выможетезаменитьmath.ldexp(x,exp) на x * 2.0^exp. Для остальных операций вы можетелибовоспользоватьсявнешнейбиблиотекой,либореализоватьихвLuaсамостоятельно.

Искатель загрузчиков модулей C, используемый require, изменилспособобработкиимен сномеромверсии.Теперь версиядолжнаидтипосле имени модуля (как это принято в большинстве другихинструментов). Для совместимости этот искатель по-прежнемупользуется старымформатом, когда он неможет найти открывающуюфункцию,соответствующуюэтомуновомустилю.(Lua5.2ужеработалтакимобразом,ноэтоизменениенебылодокументировано.)

Вызов collectgarbage("count") теперь возвращает только одинрезультат. (Вы можете вычислить второй результат из дробной частипервого.)

А.3.ИзменениявAPI

Продолжающие функции теперь принимают все, что им нужно, ввидепараметров,иненуждаютсявlua_getctx,поэтомуlua_getctxбылаубрана.Внеситесоответственныеизмененияввашкод.

У функции lua_dump появился дополнительный параметр с именемstrip. Используйте 0 как значение данного параметра, чтобы вернутьпрежнееповедение.

Функции для инъекции-проекции беззнаковых целых чисел(lua_pushunsigned, lua_tounsigned, lua_tounsignedx, luaL_checkunsigned,luaL_optunsigned) были исключены. Используйте их эквиваленты со

Page 405: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

знакомпосредствомприведениятипов.Макросы для проекции нестандартных целочисленных типов

(luaL_checkint, luaL_optint, luaL_checklong, luaL_optlong) былиисключены. Используйте их эквивалент посредством lua_Integer сприведением типа (или, если есть возможность, используйте в вашемкодеlua_Integer).

Page 406: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

ПРИЛОЖЕНИЕБ

НовоевLua5.3

Б.1.Язык

Новоевязыке:

целыечисла,поумолчанию64-битные;официальнаяподдержка32-битныхчисел;побитовыеоперации;базоваяподдержкаUTF-8;целочисленноеделение;пользовательскиеданныемогутсодержатьлюбоезначениеLuaвкачествепользовательского.

Целыечисла

У типа number два внутренних представления, или два подтипа,одинназываетсяinteger,адругойfloat.УLuaестьстрогообозначенныеправилаотом,когдаприменятькаждоеиз этихпредставлений,ноприэтом он может автоматически переводить их друг в друга. Такимобразом,упрограммистаестьвыбор:илипобольшейчастинеобращатьвнимание на разницу между целыми и вещественными числами, илиполучить полный контроль над представлением каждого числа.СтандартнаяреализацияLuaиспользуетдляподтиповintegerиfloat64-битныечисла.

Официальнаяподдержка32-битныхчисел

Возможно скомпилировать Lua так, чтобы он по умолчаниюиспользовал32-битныецелыечислаи/или32-битныечисласплавающейточкой(одинарнойточности).Опциясодновременнымвключением32бит для целых чисел и чисел с плавающей точкой особеннопривлекательна для микрокомпьютеров и встраиваемых систем.СмотритемакросLUA_32BITSвнутрифайлаluaconf.h.

Page 407: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Побитовыеоперации

Luaподдерживаетследующиепобитовыеоперации:'&'побитовоеИ'|'побитовоеИЛИ'~'побитовоевзаимоисключающееИЛИ'>>'сдвигвправо'<<'сдвигвлево'~'унарноепобитовоеНЕ

Всепобитовыеоперациипреобразуютсвоиоперандывцелыечисла,оперируютвсемибитами этихцелыхчисели выдаютрезультат в видецелогочисла.

Сдвиги вправо и влево заполняют освободившиеся биты нулями.Отрицательные смещения приводят к сдвигу в обратном направлении;смещениясабсолютнымизначениями,которыеравныилибольшечислабитов в целом числе, дают ноль (так как при этом задвигаются всебиты).

БазоваяподдержкаUTF-8

Библиотека utf8 в виде таблицы обеспечивает базовую поддержкукодировки UTF-8. Она не предлагает никакой поддержки Юникодакроме выполнения преобразования. Любая операция, которой нужнозначение символа, например его классификация, лежит вне еекомпетенции.

Если только не указано иначе, все функции, которые ожидаютпозицию байта в качестве параметра, считают, что заданная позициялибоявляетсяначаломбайтовойпоследовательности,либоравнадлинеобрабатываемой строки, увеличенной на единицу. Как и в строковойбиблиотеке,отрицательныеиндексыотсчитываютсяотконцастроки.

utf8.char(···)

Получаетнольилиболеецелыхчисел,переводиткаждоеизнихвегосоответственную последовательность байтов для UTF-8 и возвращаетстроку,вкоторойвсеэтипоследовательностисоединены.

utf8.charpattern

Данный образец (т.е. это строка, а нефункция) равен "[\0-\x7F\xC2-

Page 408: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

\xF4][\x80-\xBF]*", что соответствует ровно одной байтовойпоследовательности UTF-8 при условии, что субъект являетсядопустимойстрокойUTF-8.

utf8.codes(s)

Возвращаетзначениятакимобразом,чтоконструкцияforp,cinutf8.codes(s)dobodyend

переберетвсесимволывстрокеs,гдеpявляетсяпозицией(вбайтах),аcявляется кодовой точкой каждого символа. Вызывает ошибку, есливстречаетлюбуюнедопустимуюпоследовательностьбайтов.

utf8.codepoint(s[,i[,j]])

Возвращает кодовые точки (как целые числа) из всех символов в s,которыенаходятсямеждубайтовымипозициямиi иj (включительно).Поумолчаниюiравна1,аjравнаi.Вызываетошибку, есливстречаетлюбуюнедопустимуюпоследовательностьбайтов.

utf8.len(s[,i[,j]])

Возвращает число символов UTF-8 в строке s, которые находятсямежду байтовыми позициями i и j (включительно). По умолчанию iравна 1, а j равна -1. Если находит любую недопустимуюпоследовательность байтов, то возвращает false и позицию первогонедопустимогобайта.

utf8.offset(s,n[,i])

Возвращает позицию (в байтах), с которой начинается кодировкасимволастрокиsподномеромn(считаяотпозицииi).Отрицательныйnполучает символы до позиции i. По умолчанию i равна 1, когда n неявляется отрицательным, и #s + 1 в остальных случаях, поэтомуutf8.offset(s, -n) получит смещение n-ного символа от конца строки.Если заданный символ не находится внутри строки или сразу после ееконца,функциявозвращаетnil.

Как особый случай, когда n равен 0, функцию возвращает началокодировкисимвола,которыйсодержитi-ыйпосчетубайтстрокиs.

Данная функция полагается на то, что s является допустимойстрокойUTF-8.

Целочисленноеделение

Page 409: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Для целочисленного деления введена новая операция — '//', аоперация'/'служитдляделениявещественного.

Вещественноеделение('/')всегдапреобразуетсвоиоперандывfloatидаетврезультатеfloat.

Целочисленное деление ('//') всегда преобразует свои операнды вinteger и дает в результате integer. Преобразование происходитпосредствомокругленияоперандоввменьшуюсторону,какэтоделаетфункцияmath.floor.

Б.2.Библиотеки

Новоевбиблиотеках:

итераторipairsраспознаетметаметоды;библиотекаtableраспознаетметаметоды;опцияstripвstring.dump;функцияtable.move;функцииstring.pack,string.unpack,string.packsize.

Опцияstripвstring.dump

string.dump(function[,strip])

Возвращаетстроку,содержащуюбинарноепредставление(бинарныйкусок) заданной функции function, чтобы позже функция load,примененнаякэтойстроке,вернулакопиюэтойфункции(носновымиверхними значениями). Если параметр strip равен true, то бинарноепредставление может не включать всю отладочную информацию офункции,чтобысэкономитьместо.

Функцияtable.move

table.move(a1,f,e,t[,a2])

Перемещает элементы из таблицы a1 в таблицу a2. Эта функцияделаеттоже,чтоиследующеемножественноеприсваивание:a2[t],···= a1[f],···,a1[e]. По умолчанию a2 равна a1. Диапазон конечнойтаблицы a2 может перекрывать диапазон исходной таблицы a1.Количество перемещаемых элементов должно укладываться в целоечислоLua.

Page 410: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

Функцииstring.pack,string.unpack,string.packsize

string.pack(fmt,v1,v2,···)

Возвращает бинарную строку, содержащую значения v1, v2 и т.д.,упакованные(т.е.сериализованныевбинарнойформе)всоответствиисформатирующейстрокойfmt(см.ниже).

string.unpack(fmt,s[,pos])

Возвращает значения, упакованные в строке s (при помощиstring.pack) в соответствии с форматирующей строкой fmt (см. ниже).Необязательный параметр pos отмечает место, с которого начинаетсячтение строки s (по умолчанию равен 1). После чтения значений этафункциятакжевозвращаетиндекспервогонепрочитанногобайтавs.

string.packsize(fmt)

Возвращает размер строки, которая получается в результатеприменения string.pack с заданным форматом. Форматирующая строкаfmt(см.ниже)неможетиметьопциипеременнойдлины's'или'z'.

Форматирующая строка fmt является первым аргументомвышеперечисленныхфункций.Онаописываетразбивкусоздаваемойиличитаемой структуры. Форматирующая строка являетсяпоследовательностью конверсионных опций. Конверсионные опцииследующие:'<'задаетпорядокбайтовотмладшегокстаршему(littleendian)'>'задаетпорядокбайтовотстаршегокмладшему(bigendian)'='задаетнативныйпорядокбайтов(nativeendian)'![n]'задаетмаксимальноевыравниваниедляn(поумолчаниювыравниваниенативное)'b'signedbyte(одинсимвол,т.е.char)'B'unsignedbyte(одинсимвол,т.е.char)'h'signedshort(нативныйразмер)'H'unsignedshort(нативныйразмер)'l'signedlong(нативныйразмер)'L'unsignedlong(нативныйразмер)'j'lua_Integer'J'lua_Unsigned'T'size_t(нативныйразмер)'i[n]'signedintразмеромnбайт(поумолчаниюнативныйразмер)

Page 411: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

'I[n]'unsignedintразмеромnбайт(поумолчаниюнативныйразмер)'f'float(нативныйразмер)'d'double(нативныйразмер)'n'lua_Number'cn'строкафиксированногоразмераразмеромnбайт'z'нуль-завершеннаястрока's[n]'строка,передкоторойстоитеедлина,закодированнаяввидеunsignedinteger,равнаяnбайт(поумолчаниюnравенsize_t)'x'одинбайтдлясозданияотступа'Xop'пустойэлемент,которыйваравниваетвсоответствиисопциейop(востальныхслучаяхигнорируется)''(пустоепространство)игноририруется

(Элемент"[n]" означает необязательный целочисленный нумерал. Заисключением отступов, пробелов и кофигураций (опции "xX <=>!"),каждаяопцияотвечаетзасвойаргумент(вstring.pack)илирезультат(вstring.unpack).

Дляопций"!n","sn","in"и"In",nможетбытьлюбымцелымчисломмежду 1 и 16. Все целочисленные опции производят проверку напереполнение;string.packпроверяет,помещаетсялизаданноезначениевзаданный размер; string.unpack проверяет, помещается ли прочитанноезначениевцелоечислоLua.

Любая форматирующая строка начинается, как если бы она былапредварена "!1=", то есть с максимальным варавниванием, равным 1(отсутствиевыравнивания)инативнымпорядкомбайтов.

Выравнивание работает следующим образом: Для каждой опцииформт вставляет дополнительные отступы, пока данные не станутначинаться со смещения, которое кратно минимуму между размеромопции и максимальным выравниванием; этот минимум должен бытьстепенью 2. Опции "c" и "z" не выравниваются; опция "s" следуетвыравниваниюсвоегоначальногоцелогочисла.

Все отступы заполняются нулями функцией string.pack (иигнорируютсяstring.unpack).

Б.3.CAPI

НовоевCAPI:

болеепростойAPIдляпродолжающихфункцийвC;

Page 412: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

функцияlua_gettableиподобныеейвозвращаюттипитоговогозначения;опцияstripвlua_dump;функцииlua_geti,lua_seti,lua_isyieldable,lua_numbertointeger,lua_rotate,lua_stringtonumber.

Опцияstripвlua.dump

int lua_dump (lua_State *L, lua_Writer writer, void *data, int

strip);

Сбрасывает на диск функцию в виде бинарного куска. ПолучаетфункциюLuaсвершиныстекаипроизводитбинарныйкусок,который,если загружен повторно, дает функцию, эквивалентную ранеесброшенной. По мере производства частей куска, lua_dump вызываетзаписывателя функций (см. в справочнике lua_Writer) с указаннымиданнымидлязаписиэтихчастей.

Еслиstripравен true,бинарноепредставлениеможетневключатьвсебя какую-либо отладочную информацию о функции, чтобысэкономитьместо.

Возвращаемоезначение—этокодошибки,возвращеннойпоследнимвызовомзаписывателя;0означаетотсутствиеошибок.

ДаннаяфункцияневыталкиваетфункциюLuaизстека.

Функции

intlua_geti(lua_State*L,intindex,lua_Integeri);

Заталкивает в стек значение t[i], где t— это значение в заданноминдексе.КакивLua,даннаяфункцияможетпривестиксрабатываниюметаметодадлясобытия"index".

Возвращаеттипвставленноговстекзначения.

voidlua_seti(lua_State*L,intindex,lua_Integern);

Выполняетдействия,эквивалентныеt[n]=v,гдеt—этозначениявзаданноминдексе,аv—этозначениенавершинестека.

Даннаяфункциявытакливает значениеизстека.КакивLua,даннаяфункция может привести к срабатыванию метаметода для события"newindex".

intlua_isyieldable(lua_State*L);

Возвращает 1, если заданная сопрограмма может уступить

Page 413: Программирование на языке Luatexno.info/wp-content/uploads/2019/09/lua.pdf · 14.5. _ENV и load Упражнения 15. Модули и пакеты 15.1

управление,или0виномслучае.

intlua_numbertointeger(lua_Numbern,lua_Integer*p);

Преобразует число Lua из float в integer. Данный макрос ожидает,что n обладает целочисленным значением. Если его значение лежитвнутридиапазонацелыхчиселLua,тоонопреобразуетсявцелоечислоиприсваивается *p. Этот макрос возвращает булево значение,указывающее на то, было ли успешно преобразование. (Обратитевнимание,чтобезэтогомакросавсвязисокруглениямиданнаяпроверкадиапазонаможетневсегдадаватьожидаемыйрезультат.)

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

voidlua_rotate(lua_State*L,intidx,intn);

Циклическисдвигает элементыстекамеждудопустимыминдексомidxивершинойстека.Элементысдвигаютсянаnпозицийвнаправлениивершинывслучаеположительногоn,илина-nпозициивнаправленииоснованиявслучаеотрицательногоn.Абсолютноезначениеnнедолжнобыть больше размера сдвигаемой секции. Данная функция не можетбыть вызвана для псевдоиндекса, поскольку псевдоиндекс не являетсядействительнойпозициейвстеке.

size_tlua_stringtonumber(lua_State*L,constchar*s);

Преобразует нуль-завершенную строку s в число, заталкивает эточисло в стек и возвращает итоговый размер этой строки, т.е. ее длину,увеличенную на 1. Данное преобразование может выдать либо целоечисло, либо integer, либо float, в зависимости от лексическихсоглашений Lua. Эта строка может содержать знак, а также пробелы вначале и в конце. Если она не является допустимым нумералом, будетвозвращен 0 и заталкивания не произойдет. (Обратите внимание, чторезультатможетбытьиспользованкакбулево значение,например, true,еслипреобразованиепройдетуспешно.)

Б.4.АвтономныйинтерпретаторLua

Новоевавтономноминтерпретаторе:

Можетбытьиспользованкаккалькуляторбезнеобходимостипредварятьвыражениезнаком'=';Таблицаargдоступнавсемукоду.