Что такое оператор GOTO?
Оператор GOTO - это базовый оператор для управления потоком выполнения кода. Обычно платформа выполняет код в рамках единого блока (модуля, процедура, тело цикла) по очереди - выполнив одну команду, переходит к выполнению следующей. Но если платформа 1С видит оператор GOTO, то после него начинает выполняться строчка в произвольном месте кода, на которую ссылается этот оператор.
Указание новой точки выполнения кода происходит с помощью Меток, численно-буквенных идентификаторов, которые начинаются на символ ~ (тильда). Требования к идентификатору Метки не столь строги, как к прочим идентификаторам, и допускается делать Метки начиная с числа или даже вовсе без букв. Чтобы выполнить пометку строки, перед ней необходимо указать Метку и двоеточие. При вызове оператора GOTO вместе с ним указывается Метка и точка с запятой.
Пока Истина Цикл
Перейти ~КонецЦикла; // выходим из цикла
Сообщить("Застрял в бесконечном цикле..."); // никогда не выполнится
КонецЦикла;
~КонецЦикла:
Сообщить("Вышел из цикла!");
Переходы ограничены пределами текущих областей видимости - функциями, процедурами и модулями объектов, в которых применяется оператор GOTO. Т.е. у меток для использования переходов есть только пределы локальной видимости (напомню, что, в отличии от многих языков программирования высоких уровней, в 1С для тела Условий и Циклов нет собственной вложенной области видимости).
С появлением 8.2 появилось новое ограничение - запрещено использовать GOTO в коде управляемых приложений, которые должны выполняться на стороне клиента. Ограничение связано с отсутствием реализации оператора в веб-клиенте.
Почему у оператора Goto плохая слава в академической среде?
Изначально в низкоуровневых языках программирования операторы условных и безусловных переходов были единственно доступными операторами управления потоком выполнения. Но с появлением языков программирования высокого уровня появились новые альтернативные и более удобные операторы - Условия и Циклы, с которыми возникла парадигма Структурного Программирования.
Программисты "старой школы", которые уже имели за плечами огромный багаж программирования в машинных кодах и на версиях Ассемблера, с радостью приняли новый "синтаксический сахар", но при этом не отказались от некоторых ранее наработанных алгоритмов на переходах. Использование "не структурных операторов управления" затрудняло понимание и поддержку такого кода молодым поколением программистов, которые никогда ранее не работали с языками низкого уровня. Но было намного хуже, когда эти молодые специалисты видя операторы GOTO у старших товарищей, начинали за ними повторять и активно использовать переходы в своем коде, что должно было продемонстрировать "крутизну", но в результате только превращали их программы в трудно читаемых "монстров" со множеством ошибок. Такой код получил нарицательное прозвище "спагетти-код".
В 1966 году было опубликовано "Доказательство Бема-Якопини", суть которого заключалась в том, что любой алгоритм можно переписать без переходов по меткам на одних только условиях и циклах, хоть это может приводить к существенным потерям в эффективности. В 1968 году голландский ученый Эдсгер Дейкстра, будучи уже известным в ИТ-сообществе после своей публикации алгоритма для поиска кратчайшего пути на графе и после участия в группе разработки компилятора языка Algol, публикует свою статью "О вреде оператора GOTO" (GOTO considered harmful), которая вызвала бурные академические споры. Дейкстра утверждал, что на основе статистического анализа пришел к выводу, что количество ошибок в изученном программном коде прямо пропорционально количеству использованных в тексте программ GOTO и потому предложил радикально улучшить качество программирования введением запрета на данный оператор.
С этого момента начались гонения на оператор GOTO. И запреты на использование оператора часто были настолько фанатично слепы, что за него пришлось публично вступиться таким известным людям как Дональду Кнуту, Брайану Кернигану и Деннису Ритчи, Линусу Торвальдсу, Стиву МакКоннелли и многим другим чуть менее известным. Эти авторитетные люди заявили, что оператор GOTO улучшает скорость, размер и ясность кода программы, но только при разумном использовании разумным программистом.
Любопытные факты. Общеизвестно, что Дейкстра был мало заинтересован в приеме на старшие курсы университета, где он преподавал, студентов со знанием Фортрана по той причине, что вместе с этими знаниями могли привиться дурные привычки программирования. Под дурной привычкой имелось в виду, конечно же, применение GOTO, правила использования которого в Фортране было очень свободным. В результате давления порицанием от научного сообщества в 1978 году в Фортране сначала запретили свободный вход/выход в рамках циклов (до сих пор разрешен в C++), а потом в 1995 запретили почти все прочие варианты переходов.
Откуда у оператора GOTO плохая слава в 1С?
Почему с оператором GOTO все не так однозначно в универсальных высокоуровневых языках, на которых до сих пор делают ассемблерные вставки, более-менее понятно. Но почему в сообществе 1С при виде использования оператора перехода начинается настоящая истерика с криками "четвертовать", "сжечь", "повесить", "волчий билет и запрет на программирование"?
Откуда столько негатива? Ведь язык 1С - это DSL, который не предполагает написание сложных алгоритмов, а потому большая часть программистов пишет простые скрипты и скорее всего даже не догадывается про существование GOTO, а остальные просто помнят о его существовании, но при этом превосходно обходятся без него.
Причина №1 - Академический навык. Далеко не все программисты 1С самоучки или свитчеры из финансистов. Есть те, кто окончили кафедры компьютерных наук и из-за событий непреодолимой силы попали в 1С. А гонения на оператор GOTO - это классика современных компьютерных наук. Таким людям на лекциях сказали, что использование GOTO - это плохо. Если они на экзамене смогли воспроизвести данное утверждение, то получали хорошую оценку и закрепляли навык порицания.
Причина №2 - Портирование внешнего кода. Часто при работе с данными в бинарных форматах, драйверами торгового оборудования и с прочими библиотеками, от поставщика есть пример кода, который легко скопировать в 1С и который после этого даже будет работать. Но ради эффективности такой код часто низкоуровневый и содержит множественные GOTO. Если потом неподготовленному человеку потребуется что-то там исправить, то возникает состояние "плачу кровавыми слезами" и кредо "никогда в жизни не буду связываться с GOTO".
Причина №3 - Обфускация кода. Когда хотят затруднить самостоятельное использование кода клиентом и обеспечить дополнительный доход для поддержки, то код запутывают. Есть несколько вариантов запутывания на уровнях исходного кода и байткода, но практически все варианты для затруднения анализа используют множественные GOTO. Когда такой код видят специалисты клиента, то он вызывает высокий уровень раздражения. Когда такой запутанный код работает с ошибками, которые нужно исправить, а "поставщик" исчез, то раздражение возрастает на порядок.
Любопытные факты. На уровне байткода не существуют никаких структурных операторов, а все условия, циклы, обработки ошибок и процедуры с функциями реализованы на условных и безусловных переходах. Т.е. байткод 1С сильно подобен ассемблеру. Выходит, что изначально для платформы 1С оператор GOTO является "родным", а циклы и условия по своей сути являются "синтаксическим сахаром" для удобства программистов.
Когда можно и нужно использовать GOTO в 1С?
Предыдущий раздел дает часть ответа. Так если в портируемом алгоритме были GOTO, то проще их сохранить, а не терять несколько дней на рефакторинг с риском привнесения ошибок.
Так же без GOTO никуда, если вы решили писать собственную версию обфускатора. Но не рекомендую таким заниматься. Во первых, ниша уже занята и у вас практически нет шансов совершить революцию, чтобы потеснить конкурентов и хотя бы отбить затраты на разработку (даже для себя проще купить чужое решение, чем тратить время на разработку). А во вторых, тех, кто обфусцирует собственный код, никто не любит - это может существенно испортить деловую репутацию (кто занимается отраслевыми решениями меня поймут).
Но все же, а какие адекватные причины могут побудить использовать GOTO при написании своего кода? Предлагаю следующий перечень по возрастанию степени ответственности за принятое решение:
1. Сложный входящий алгоритм
Не всегда мы придумываем свои алгоритмы самостоятельно, иногда нам ставят задачу просто кодировать согласно чужим "блок-схемам". Это может быть как адаптация "под 1С" чужого кода на универсальном языке программирования. Или это может постановка от пользователя, который именно "так" видит решение. И эти внешние алгоритмы могут содержать переходы в произвольные части кода, которые можно реализовать исключительно с помощью GOTO. Что-то такое:
Только на схеме маленький и простой вариант, который легко переделать (о переделке еще вспомним в пункте №3). Но представьте, что к вам пришел руководитель кадровой службы и попросил сделать для него мегакрутую штатную расстановку, на которой будет ВСЁ, и в описании которой десятки таких странных "поворотов сюжета".
Вы можете заявить, что такой алгоритм стоит предварительно "причесать" - структурировать, выровнять линии управления, возможно упростить некоторые условия согласно карт Карно. Но хватит ли у вас квалификации и знания предметной области для написания своего алгоритма? А если вам платят только за программирование, то готовы ли несколько вечеров забрать у своей семьи и жить на работе? А как будете поддерживать написанное? А если к вам снова придет заказчик и скажет - "тут ещё работы на 5 минут", просто добавь в этом блоке новое условие под "новый вчера принятый закон" и утром уже должно быть в "проде" или иначе компанию ждут гигантские штрафы, но вы изначально не учли вероятность изменения законов и для вашего нового алгоритма теперь нужна неделя на "всё переписать"?
Если ответственность за создание алгоритма на другом человеке, то чаще лучше не требовать её на себя и не привносить лишние риски на свою "пятую точку". В конце-концов с нами тоже работают профессионалы и их алгоритмы могут являться не результатом безумия, а компромиссом между "понятностью" и "внешними условиями".
2. Быстрый выход из вложенных циклов
Мало кто знает, но операторы Прервать (Break) и Продолжить (Continue) - это тоже операторы безусловного перехода, подобные GOTO. Первый совершает переход на скрытую метку после цикла, а второй на окончание цикла перед переходом на следующую итерацию. Для нас это чрезвычайно удобные переходы уже как минимум за счет того, что не нужно явно ставить метки и следить за уникальностью их имен - все происходит автоматически. Но не всегда циклы одноуровневые!
Не очень часто, но все же время от времени нужно писать циклы, где уровней очень много - например, обработка данных с группировкой по периодам, видам контрагентов, самим контрагентам, валютам, договорам, заказам и т.д. И далее предположим, что для нашей бизнес-задачи погашения долгов нужно отобрать из этой древовидной структуры данные на определенную сумму, но с хитрым условием, которое прописали юристы в договоре - нужно возвращать деньги за полные поставки с сортировками сначала по дате заключения заказа, а потом по дате фактического прибытия груза. Лимит по сумме тоже хитрый - поскольку и мы и поставщики транснациональные компании, то долги нужно возвращать в разных валютах и при этом действуют независимые лимиты на уровне валюты (ограничения от банков) и в целом по управленческой валюте (ограничение от своих финансистов).
Как обычно делают такие "прыжки" через несколько уровней цикла? На каждом уровне создаются сигнальные переменные, значение которых проверяем при завершении тела цикла. Если значение переменной изменилось, то прерываем выполнение текущего уровня и переходим к проверке значений переменных выше. Но чем больше уровней, чем сложнее будет логика "возврата" - слишком много таких переменных и блоков кода по их проверке.
Хотя намного проще в такой ситуации было бы поставить метки "СледующаяВалюта" и "СледующийКонтрагент".
По сути это все еще тот же Прервать, только сразу на нужный нам уровень.
3. Отказ от избыточных подпрограмм (процедур).
Как упоминалось ранее, любой алгоритм с GOTO можно переписать в алгоритм с циклами и условиями. Но что мы получим в результате? Давайте рассмотрим простой пример такого преобразования:
Сразу становится видимой главная проблема - дублирование участков кода. Теперь блок инструкций B вставлен два раза, а блок C - три раза.
Можно так и оставить, но сильно усложняется поддержка - ведь при изменении одного блока, нужно искать его дубликаты и вносить туда аналогичные правки. Автор лично видел очень много подобного дублируемого кода, где "синхронизацией" не заморачивались, что и приводило к проблемам из-за side-effects.
Как известно из теории компьютерных наук, правильным подходом при наличии дублирующих участков является выделение их в отдельные подпрограммы - процедуры, если говорить терминами 1С. Ярким примером, который знаком всем, является расчет стоимости по цене и количеству - алгоритм расчета выносим в отдельную процедуру, которую вызываем не только в рамках текущего алгоритма (например в заполнении по основанию), но и при последующем программировании логики (например в обработчиках изменения количества на форме). При чем для алгоритма не важно, что было передано ему в качестве параметра и он одинаково отработает для табличных частей Товары, Работы, Материалы, Продукция или Полуфабрикаты.
Но не каждая процедура такая универсальная как расчет суммы по цене и количеству. Если нужно выполнить более сложные действия, то тут или нужно усложнять сигнатуру метода и переписывать все вызовы (и потом ловим ошибки в процессе работы, так как благодаря отсутствию типизации в 1С нет контроля на передачу сложных параметров), или внутри процедуры обращаться к метаданным конфигурации и выполнять действия в зависимости от типов и состава реквизитов (и потом получить медленную программу из-за высоких накладных расходов по работе с метаданными), или писать в модули десяти служебных подпрограмм (и получить захламленный и сложно читаемый модуль, так как в 1С не предусмотрено хранение иерархического кода - выделение в Регионы интересная попытка, но пока очень сырая).
А ведь еще не упомянули про то, что выделение кода в подпрограмму - это не "бесплатно". За передачу параметров, за создание нового изолированного контекста и за обслуживание стека вызовов нужно платить дополнительным расходом памяти и процессорного временем.
Любопытные факты. В 1С упомянутый стек вызовов не просто слегка замедляет выполнение программы, но еще и может привести к ее падению, когда он окажется переполнен. В результате просто невозможно реализовать некоторые классы рекурсивных алгоритмов обработки данных - при увеличении входящего потока информации каждый раз получим падения. Из-за этого реализацию действительно "глубоких" рекурсий на 1С можно сделать исключительно с помощью GOTO.
Но вернемся к самому началу - к преобразованию алгоритма на картинке. Мы действительно можем убрать GOTO и за счет обслуживающих "замену" инструментов слегка замедлить выполнение - пусть с 10 миллисекунд до 1 секунды. Какая мелочь - 0,99 секунды ведь никто не заметит, это же "квантовое измерение" и замедление в 100 раз это ни о чем. Хотя СТОП! А если этот маленький кусочек кода находится в цикле, который при обычных условиях выполняется один час (какой-нибудь парсинг цен конкурентов), то теперь этот цикл займет 100 часов, или другими словами остановит работу на четыре дня вместо традиционного перекура, к которому уже все привыкли.
Итого. Если у нас есть возможность "почистить" модуль от десятка служебных процедур, сделать код алгоритма более читаемым, а общее время выполнение ускорить на один-два порядка, то не вижу здравых причин от этого отказываться. Зачем делать закрытие месяца с расчетом себестоимости продукции на протяжении дня-недели, если есть возможность ускорить до часа-двух?
4. Создание динамических алгоритмов (метапрограммирование).
Программистам на языке 1С не часто, но все же попадаются интересные и сложные задачи. Сложные не из-за большого объема или необходимости изучения новой документации, а из-за отсутствия фиксированного алгоритма решения. Это потребность создавать динамические алгоритмы, ход выполнения которых зависит от неких внешних условий.
Это может быть код, который генерируется "на лету" для обменов по "Конвертация данных 2". Или это может быть интеграция по некоторому хитрому формату внешних данных, правила обработки которого являются частью данного формата. Или возможно вы делаете NoCode-инструмент, чтобы ваши пользователи сами писали сложную обработку данных просто накликивая требуемую им блок-схему.
Т.е. у вас может быть только информация о базовом наборе "кирпичиков" для будущего алгоритма и правила для их объединения, но никаких догадок о том, в какие финальные конфигурации они будут складываться.
Подобное метапрограммирование на 1С делается двумя способами:
- формированием текста будущей программы и запуск ее с помощью команды Выполнить (Execute);
- формированием текста будущей программы, запись ее во внешнюю обработку с помощью популярной утилиты v8unpack, а затем запуск этой обработки.
У первого способа есть ограничения, которые накладываются платформой 1С:
- отсутствие вложенных контекстов и работа только с контекстом точки вызова;
- отсутствие возможности создания процедур и функций;
- запрет на использование ключевого слова Возврат (Return).
Команда Выполнить изначально задумывалась для запуска фрагмента, а не полноценного кода, и потому тут использование операторов GOTO максимально облегчит нашу работу. Во первых, упрощается стыковка "кирпичиков" - на вход следующего фрагмента вставляем метку, а на выходе из требуемых предыдущих блоков делаем переход на эту метку. Во вторых, при описании содержимого самих "кирпичиков" мы можем избежать самоповтора кода (см. часть "Отказ от избыточных подпрограмм").
У второго способа (создание внешней обработки) нет никаких платформенных ограничений и мы можем генерировать полноценный код согласно парадигме структурного программирования без всяких, так сказать, "мерзких" GOTO. Но есть ряд нюансов, с которыми нужно будет сперва побороться:
- Этот метод в целом не поддерживается и не поощряется фирмой 1С. Утилита v8unpack (и ей подобные) создана независимыми программистами, которые предоставляют свою работу в OpenSource по модели "AS IS" - т.е. если у вас возникают проблемы, то решение их будет вашей личной задачей.
- Установка и запуск утилиты v8unpack на сервере 1С может быть связана со сложностями. Особенно если это не ваш сервер, а местные системные администраторы очень настороженно относятся к малоизвестным программам из интернета.
- Запись внешней обработки на диск и её последующее чтение могут быть заблокированы антивирусом или сбоем файловой подсистемы.
- При загрузке внешних обработок иногда происходят сбои. Если вы каждый раз не генерируете новое уникальное название, то часто программный кэш не очищается и 1С работает со старыми версиями обработок. Но и смена названий не панацея - иногда внешние обработки по непонятным причинам просто не работают (возможно сбои реализации защищенного режима?).
Заключительное слово
Позиция автора статьи предельно прозрачна - не нужно быть столь категоричными к оператору GOTO. Иногда с ним код становится красивым и более понятным. Иногда использование GOTO экономит часы и дни разработки. Иногда его наличие позволяет оптимизировать сложные расчеты и выполнить их в разумные сроки.
У нас на платформе 1С и так слишком много технологических ограничений, чтобы мы еще дополнительно себя ограничивали. Главное не боятся взять на себя ответственность и предварительно дать утвердительные ответы на следующие вопросы:
- точно ли будет ощутимый эффект от применения GOTO?
- точно ли ваш код будет понятен другим, кто станет его поддерживать?
- хорошо ли вы продумали ваш алгоритм и уверены в отсутствии "побочных эффектов"?
post scriptum
Автор статьи достаточно видел применение GOTO в различном коде (даже в популярных Инструментах разработчика), но при этом ни разу не сталкивался с проблемами, которые вызвал именно факт его использования. Возможно у читателей есть подобный опыт? Буду чистосердечно благодарен, если поделитесь в комментариях.