В среде разработчиков на платформе «1С:Предприятие» термин «костыль» давно перестал быть исключительно ругательным. Зачастую это проверенное временем, прагматичное решение, позволяющее обойти архитектурные ограничения платформы или оптимизировать производительность системы там, где штатные механизмы работают неидеально. Некоторые из таких решений настолько прижились, что вошли в состав Библиотеки Стандартных Подсистем (БСП) или со временем были реализованы на уровне самой платформы.
В этой статье мы подробно, по шагам разберем самые известные «костыли» в конфигурациях 1С, проанализируем физику процессов, которые они призваны исправить, изучим примеры программного кода и рассмотрим современные альтернативные решения.
---
Одним из самых частых и одновременно опасных приемов в коде начинающих разработчиков является получение значений реквизитов объекта через точку от ссылки, например: Организация.ИНН или Договор.Владелец. Рассмотрим подробнее, почему этот интуитивно понятный подход может привести к катастрофическому падению производительности информационной системы.
Когда мы обращаемся к реквизиту объекта через его ссылку (например, ДатаДокумента = СсылкаНаДокумент.Дата), платформа «1С:Предприятие» выполняет кэширование. Физически это означает следующее:
Такой механизм задумывался как «насильная оптимизация» для облегчения написания кода программистами не самого высокого уровня. Предполагалось, что лучше один раз прочитать объект из базы, чем делать 10 мелких запросов к разным полям. Однако на практике этот подход имеет огромный минус. Представьте себе тяжелый документ (например, «Начисление зарплаты» или «Перемещение товаров») с табличной частью в несколько тысяч строк. Обращение к простому реквизиту шапки (например, Проведен или Номер) заставит СУБД прочитать, а сервер приложений — передать по сети и удержать в оперативной памяти мегабайты информации. Особую опасность представляют реквизиты с типом ХранилищеЗначения, в которых могут храниться тяжелые файлы или картинки.
Для решения этой проблемы разработчики используют два основных обходных пути.
Запрос позволяет СУБД извлечь строго определенные поля без чтения табличных частей и лишней нагрузки на сеть и память:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СсылкаНаОбъект.ИНН КАК ИНН,
| СсылкаНаОбъект.КПП КАК КПП
|ИЗ
| Справочник.Организации КАК СсылкаНаОбъект
|ГДЕ
| СсылкаНаОбъект.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", ОрганизацияСсылка);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ИНН = Выборка.ИНН;
КПП = Выборка.КПП;
КонецЕсли;
Для того чтобы не писать громоздкие запросы ради одного-двух реквизитов, разработчики БСП внедрили специальную обертку, которая под капотом строит и выполняет оптимальный запрос. Мы можем использовать ее в одну строку:
ПараметрыОбъекта = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(ОрганизацияСсылка, "ИНН, КПП");
ИНН = ПараметрыОбъекта.ИНН;
КПП = ПараметрыОбъекта.КПП;
Этот метод полностью нивелирует оверхед от неявного кэширования объекта и является стандартом разработки для высоконагруженных систем.
---
Долгое время в языке программирования 1С отсутствовал штатный оператор или метод для приостановки выполнения потока программы (аналог Sleep() или Delay() в других языках). Потребность в паузах возникает регулярно: при ожидании завершения фонового процесса, отправке повторных запросов к внешним API (чтобы не превысить лимиты частоты обращений), работе с торговым оборудованием и т.д.
Разберем три классических костыля для реализации задержки и одно современное штатное решение.
Самый простой, но категорически запрещенный в промышленной разработке способ:
Процедура ПаузаАктивная(Секунд)
ВремяНачала = ТекущаяДата();
Пока ТекущаяДата() - ВремяНачала < Секунд Цикл
// Пустой цикл без остановки процессора
КонецЦикла;
КонецПроцедуры;
Почему так делать нельзя: Этот код полностью загружает одно ядро процессора на 100% выполнением бесполезных итераций цикла. Если такой код запустится на сервере приложений в нескольких сеансах одновременно, это приведет к полному зависанию сервера.
Более продвинутая вариация этого метода с использованием миллисекунд выглядит следующим образом:
Процедура ПаузаВМиллисекундах(ВремяВМиллисекундах) Экспорт
Если Не ЗначениеЗаполнено(ВремяВМиллисекундах) Тогда
Возврат;
КонецЕсли;
ВремяНачала = ТекущаяУниверсальнаяДатаВМиллисекундах();
ПредыдущееВремяВыполнения = 0;
Пока Истина Цикл
ВремяОкончания = ТекущаяУниверсальнаяДатаВМиллисекундах();
ВремяВыполнения = ВремяОкончания - ВремяНачала;
Если ВремяВыполнения < 0 Тогда
ВремяВыполнения = -ВремяВыполнения;
КонецЕсли;
Если (ВремяВыполнения >= ВремяВМиллисекундах)
Или (ПредыдущееВремяВыполнения > ВремяВыполнения) Тогда
Прервать;
КонецЕсли;
ПредыдущееВремяВыполнения = ВремяВыполнения;
КонецЦикла;
КонецПроцедуры;
Несмотря на высокую точность, этот метод сохраняет главный недостаток — бессмысленную утилизацию вычислительных ресурсов сервера.
Популярный метод, который часто применялся на стороне Windows-клиентов:
Процедура ПаузаЧерезскрипт(Секунд)
Попытка
Шелл = Новый COMОбъект("WScript.Shell");
Команда = "ping -n 1 -w " + Формат(Секунд * 1000, "ЧГ=0") + " 127.0.0.1";
Шелл.Run(Команда, 0, Истина);
Исключение
// Обработка ошибки, если COM-объект недоступен
КонецПопытки;
КонецПроцедуры;
Или через прямой запуск утилиты sleep через скрипты Windows Script Host:
scr = Новый COMОбъект("WScript.Shell");
scr.Run("sleep " + Формат(ЗадержкаСекунд, "ЧДЦ=0; ЧГ="), 0, 1);
Минусы решения: Код жестко привязан к операционной системе Windows. На серверах под управлением Linux данный код мгновенно завершится аварийно. Кроме того, создание COM-объекта — достаточно ресурсоемкая операция.
Наиболее изящный способ реализации задержки на сервере без привязки к ОС и без паразитной загрузки процессора. Он использует системный метод ожидания завершения фонового задания:
Процедура ПаузаФоновое(Секунд) Экспорт
ТекущийСеанс = ПолучитьТекущийСеансИнформационнойБазы();
ФоновоеЗадание = ТекущийСеанс.ПолучитьФоновоеЗадание();
// Если мы уже внутри фонового задания, запускаем рекурсивно еще одно
Если ФоновоеЗадание = Неопределено Тогда
Параметры = Новый Массив;
Параметры.Добавить(Секунд);
// "МойОбщийМодуль" должен иметь флаг "Вызов сервера"
ФоновоеЗадание = ФоновыеЗадания.Выполнить("МойОбщийМодуль.ПаузаФоновое", Параметры);
КонецЕсли;
Попытка
// Метод ОжидатьЗавершения приостанавливает поток исполнения сеанса
ФоновоеЗадание.ОжидатьЗавершения(Секунд);
Исключение
// Исключение сработает по таймауту, что нам и нужно для паузы
Возврат;
КонецПопытки;
КонецПроцедуры;
Как это работает: Платформа останавливает выполнение текущего потока и переводит его в состояние ожидания завершения фонового задания на указанное количество секунд. Процессорное время при этом не расходуется.
Начиная с версии платформы 8.3.25 разработчики 1С услышали сообщество и добавили штатный метод глобального контекста ВызватьПаузу(ВремяПаузы) (в англоязычной версии — CallSleep(SleepTime)). Параметр задается в миллисекундах. Этот метод работает на клиенте, на сервере и в фоновых заданиях, полностью заменяя все вышеперечисленные обходные варианты.
---
В языке 1С отсутствует синтаксический сахар для создания массивов со списком значений в одну строку (как это реализовано во многих современных языках программирования в виде литералов массивов [val1, val2, val3]). Стандартный способ создания массива в 1С выглядит избыточно длинным:
МассивТипов = Новый Массив;
МассивТипов.Добавить(Тип("Строка"));
МассивТипов.Добавить(Тип("Число"));
МассивТипов.Добавить(Тип("Дата"));
В известной библиотеке «Инструменты разработчика» (автор — ТормозИТ) был предложен изящный костыль, позволяющий инициализировать массив одной строкой кода:
Функция БыстрыйМассивЛкс(
п1 = Неопределено, п2 = Неопределено, п3 = Неопределено, п4 = Неопределено, п5 = Неопределено,
п6 = Неопределено, п7 = Неопределено, п8 = Неопределено, п9 = Неопределено, п10= Неопределено,
п11= Неопределено, п12= Неопределено, п13= Неопределено, п14= Неопределено, п15= Неопределено,
п16= Неопределено, п17= Неопределено, п18= Неопределено, п19= Неопределено, п20= Неопределено
) Экспорт
М = Новый Массив();
Если п1 = Неопределено Тогда Возврат М; Иначе М.Добавить(п1 ); КонецЕсли;
Если п2 = Неопределено Тогда Возврат М; Иначе М.Добавить(п2 ); КонецЕсли;
Если п3 = Неопределено Тогда Возврат М; Иначе М.Добавить(п3 ); КонецЕсли;
Если п4 = Неопределено Тогда Возврат М; Иначе М.Добавить(п4 ); КонецЕсли;
Если п5 = Неопределено Тогда Возврат М; Иначе М.Добавить(п5 ); КонецЕсли;
Если п6 = Неопределено Тогда Возврат М; Иначе М.Добавить(п6 ); КонецЕсли;
Если п7 = Неопределено Тогда Возврат М; Иначе М.Добавить(п7 ); КонецЕсли;
Если п8 = Неопределено Тогда Возврат М; Иначе М.Добавить(п8 ); КонецЕсли;
Если п9 = Неопределено Тогда Возврат М; Иначе М.Добавить(п9 ); КонецЕсли;
Если п10= Неопределено Тогда Возврат М; Иначе М.Добавить(п10); КонецЕсли;
Если п11= Неопределено Тогда Возврат М; Иначе М.Добавить(п11); КонецЕсли;
Если п12= Неопределено Тогда Возврат М; Иначе М.Добавить(п12); КонецЕсли;
Если п13= Неопределено Тогда Возврат М; Иначе М.Добавить(п13); КонецЕсли;
Если п14= Неопределено Тогда Возврат М; Иначе М.Добавить(п14); КонецЕсли;
Если п15= Неопределено Тогда Возврат М; Иначе М.Добавить(п15); КонецЕсли;
Если п16= Неопределено Тогда Возврат М; Иначе М.Добавить(п16); КонецЕсли;
Если п17= Неопределено Тогда Возврат М; Иначе М.Добавить(п17); КонецЕсли;
Если п18= Неопределено Тогда Возврат М; Иначе М.Добавить(п18); КонецЕсли;
Если п19= Неопределено Тогда Возврат М; Иначе М.Добавить(п19); КонецЕсли;
Если п20= Неопределено Тогда Возврат М; Иначе М.Добавить(п20); КонецЕсли;
Возврат М;
КонецФункции
Теперь мы можем использовать этот метод для создания и инициализации массивов на лету, что делает код лаконичным, особенно при инициализации сложных объектов:
ОписаниеТиповСтрокаЧисло = Новый ОписаниеТипов(
БыстрыйМассивЛкс(Тип("Строка"), Тип("Число"))
);
Ограничение костыля: Если одним из элементов массива должно быть реальное значение Неопределено, алгоритм функции прервется раньше времени и вернет незаполненный до конца массив. Кроме того, данный подход ухудшает читаемость кода при передаче большого количества параметров.
---
В процессе интеграции 1С с внешними информационными системами (через HTTP/REST API, веб-сервисы или файлы) часто возникает задача поиска или создания объектов по их GUID (уникальному идентификатору). Нам регулярно приходится преобразовывать входящую строку в объект типа УникальныйИдентификатор.
Проблема заключается в том, что конструктор Новый УникальныйИдентификатор(СтрокаГUID) падает с критической ошибкой, если переданная строка не соответствует строгому формату GUID (длина, дефисы, шестнадцатеричные символы). Чтобы избежать падения системы, разработчики вынуждены использовать конструкцию Попытка ... Исключение:
Попытка
Идентификатор = Новый УникальныйИдентификатор(ВходящаяСтрока);
Исключение
Идентификатор = Неопределено; // Некорректный формат строки
КонецПопытки;
Однако регулярное использование блока Попытка снижает производительность системы при массовой обработке данных, так как перехват исключений на уровне платформы — ресурсоемкая операция. Для решения этой проблемы в БСП добавлена вспомогательная функция проверки валидности строки регулярным выражением (или прямым посимвольным анализом):
Функция ЭтоУникальныйИдентификатор(Знач Значение) Экспорт
// GUID должен состоять из 36 символов определенного формата:
// XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX, где X = [0..9, a..f, A..F]
Если ТипЗнч(Значение) <> Тип("Строка") Тогда
Возврат Ложь;
КонецЕсли;
Шаблон = "^\s*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\s*$";
// В БСП для этого используется RegExp или посимвольный перебор
// Рассмотрим вариант посимвольной проверки для наглядности:
СтрокаПроверки = СокрЛП(Значение);
Если СтрДлина(СтрокаПроверки) <> 36 Тогда
Возврат Ложь;
КонецЕсли;
Для Позиция = 1 По 36 Цикл
Символ = Сред(СтрокаПроверки, Позиция, 1);
Если Позиция = 9 Или Позиция = 14 Или Позиция = 19 Или Позиция = 24 Тогда
Если Символ <> "-" Тогда
Возврат Ложь;
КонецЕсли;
Иначе
Если Найти("0123456789abcdefABCDEF", Символ) = 0 Тогда
Возврат Ложь;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Истина;
КонецФункции
Использование такой функции перед созданием объекта типа УникальныйИдентификатор страхует наш код от падений и избавляет от необходимости генерировать тяжелые системные исключения.
---
В управляемом приложении 1С объект типа ТаблицаЗначений доступен исключительно на стороне сервера. Мы не можем передать его на клиент напрямую в качестве возвращаемого значения безконтекстной серверной функции. Попытка сделать это вызовет ошибку передачи данных между клиентом и сервером.
Для обхода этого ограничения разработчики используют прием конвертации таблицы значений в массив структур, который без проблем сериализуется и передается на клиент:
&НаСервереБезКонтекста
Функция ПолучитьДанныеДляКлиента()
Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("Код");
Таблица.Колонки.Добавить("Наименование");
// Заполняем таблицу данными...
НоваяСтрока = Таблица.Добавить();
НоваяСтрока.Код = "00001";
НоваяСтрока.Наименование = "Тестовый товар";
// Преобразуем в массив структур
МассивРезультат = Новый Массив;
Для Каждого СтрокаТЗ Из Таблица Цикл
СтруктураСтроки = Новый Структура;
Для Каждого Колонка Из Таблица.Колонки Цикл
СтруктураСтроки.Вставить(Колонка.Имя, СтрокаТЗ[Колонка.Имя]);
КонецЦикла;
МассивРезультат.Добавить(СтруктураСтроки);
КонецЦикла;
Возврат МассивРезультат;
КонецФункции
На клиенте мы получаем обычный массив структур, данные из которого можно легко загрузить в реквизит формы типа ДанныеФормыКоллекция (например, в табличную часть формы или таблицу значений на форме):
&НаКлиенте
Процедура ЗагрузитьДанные()
МассивДанных = ПолучитьДанныеДляКлиента();
Для Каждого СтрокаДанных Из МассивДанных Цикл
НоваяСтрокаФормы = ДанныеФормыТаблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаФормы, СтрокаДанных);
КонецЦикла;
КонецПроцедуры
При передаче очень больших объемов данных вместо массива структур эффективнее сериализовать таблицу значений в JSON-строку на сервере, передать строку на клиент и там распарсить ее штатными средствами чтения JSON.
---
При записи данных в непериодический или периодический регистр сведений разработчики часто сталкиваются с дилеммой выбора объекта платформы: использовать РегистрСведенийМенеджерЗаписи или РегистрСведенийНаборЗаписей.
Менеджер записи удобен и лаконичен, когда нужно быстро записать одну строку по известным измерениям:
МенеджерЗаписи = РегистрыСведений.КурсыВалют.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДата();
МенеджерЗаписи.Валюта = СсылкаНаВалюту;
МенеджерЗаписи.Курс = 100;
МенеджерЗаписи.Записать();
Однако у Менеджера записи есть серьезное ограничение: у него отсутствуют такие важные свойства и методы набора записей, как свойство ОбменДанными.Загрузка = Истина (необходимое для обхода проверок при загрузке данных) и коллекция ДополнительныеСвойства (используемая для передачи параметров в обработчики событий).
Чтобы сохранить лаконичность кода менеджера записи, но при этом иметь доступ к полному функционалу набора записей, разработчики используют костыль-обертку, программно трансформирующую менеджер записи в набор:
Процедура ЗаписатьЗаписьРегистраСОбменомДанными(Валюта, Курс, Период)
НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
// Устанавливаем отборы по ключевым измерениям
НаборЗаписей.Отбор.Валюта.Установить(Валюта);
НаборЗаписей.Отбор.Период.Установить(Период);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Валюта = Валюта;
НоваяЗапись.Период = Период;
НоваяЗапись.Курс = Курс;
// Включаем режим загрузки для отключения триггеров и проверок
НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать(Истина); // Истина указывает на необходимость замещения
КонецПроцедуры
Данный подход гарантирует стабильность записи данных при интеграционных обменах, исключая срабатывание тяжелых обработчиков событий ПередЗаписью или ПриЗаписи в модулях набора записей регистра.
---
Изучение и понимание принципов работы подобных обходных приемов позволяет нам писать более осознанный и производительный код. Большинство «костылей» рождаются из-за стремления разработчиков защитить систему от неоптимального поведения базовых механизмов СУБД или платформы. Внедряя такие решения в свои проекты, важно помнить о границах их применимости и своевременно заменять их на новые штатные функции, которые регулярно поставляет фирма «1С» с выходом новых версий платформы и БСП.