При разработке механизмов распределения сумм, затрат или скидок в 1С программисты часто сталкиваются с задачей сопоставления строк табличной части документа и элементов массива, в котором хранятся рассчитанные показатели. Начинающие разработчики нередко совершают критическую ошибку, вкладывая один цикл в другой. Это приводит к катастрофическому падению производительности информационной базы.
В этой статье мы подробно разберем, почему вложенные циклы — это «антипаттерн», проанализируем популярное быстрое решение с форума, выявим его скрытые угрозы и напишем идеальный, безопасный и максимально производительный код в соответствии со стандартами разработки фирмы «1С».
Рассмотрим исходный фрагмент кода, с которым автор обратился на форум:
Если Не СуммаРаспределения = Неопределено Тогда
Для Каждого СтрокаТЧ Из ЭтотОбъект.Товары Цикл
Для i = 0 По ЭтотОбъект.Товары.Количество()-1 Цикл
ЭлементМассива = СуммаРаспределения[i];
Проводка = Проводки.Добавить();
Проводка.Период = ЭтотОбъект.Дата;
Проводка.Организация = ЭтотОбъект.Организация;
Проводка.СчетДт = Счет15_02;
Курс = МодульВалютногоУчета.ПолучитьКурсВалюты(ВалютаДокумента, Дата);
Проводка.Сумма = Окр(СтрокаТЧ.Сумма*Курс.Курс/Курс.Кратность,4)-(ЭлементМассива);
КонецЦикла;
КонецЦикла;
КонецЕсли;
Проанализируем этот алгоритм. Здесь мы видим конструкцию Для Каждого СтрокаТЧ Из ЭтотОбъект.Товары, внутри которой запущен еще один цикл Для i = 0 По ЭтотОбъект.Товары.Количество()-1. Что это означает на практике?
Если в табличной части Товары содержится 100 строк, то внешний цикл выполнится 100 раз. Но на каждой итерации внешнего цикла внутренний цикл выполнится еще 100 раз! В итоге тело внутреннего цикла (включая создание проводок и обращение к базе данных за курсом валюты) выполнится 10 000 раз (100 * 100). Программа создаст дублирующие проводки, зациклится и, скорее всего, зависнет. Сложность такого алгоритма оценивается как O(N²), что недопустимо для бизнес-приложений.
В сообщении 6 автору предложили простое и изящное на первый взгляд решение — избавиться от внутреннего цикла и обращаться к элементу массива напрямую по индексу строки:
ЭлементМассива = СуммаРаспределения[СтрокаТЧ.НомерСтроки - 1];
Действительно, это сокращает сложность алгоритма до линейной O(N). Теперь цикл будет один, и количество итераций будет в точности равно количеству строк в табличной части. Однако у этого быстрого решения есть три серьезных уязвимости, которые могут «выстрелить» в реальной работе предприятия.
СуммаРаспределения по какой-то причине вернет меньше элементов, чем строк в табличной части (например, из-за округления или исключения нулевых строк), программа аварийно завершит работу с ошибкой у пользователя.НомерСтроки: Номер строки жестко привязан к физическому порядку строк в табличной части на форме. Если перед выполнением этого кода табличная часть была отсортирована пользователем, скопирована или из нее были программно удалены строки, синхронизация между индексом массива и номером строки нарушится. Это приведет к тому, что суммы распределятся не на те товары.МодульВалютногоУчета.ПолучитьКурсВалюты(ВалютаДокумента, Дата) осталась внутри цикла. Метод получения курса делает запрос к базе данных (к регистру сведений). Запрос в цикле — это грубейшее нарушение стандартов «1С». Если в документе 500 строк, система сделает 500 отдельных запросов к СУБД, что вызовет сильные задержки (блокировки базы данных).Проанализируем, как перестроить этот алгоритм так, чтобы он работал мгновенно и не приводил к системным сбоям.
Параметры ВалютаДокумента и Дата одинаковы для всего документа и не меняются от строки к строке. Следовательно, получить курс валюты нужно один раз до начала цикла и сохранить его в переменную. Это сократит количество обращений к базе данных с N раз до 1 раза.
Вместо обхода коллекции через Для Каждого и привязки к свойству НомерСтроки, мы организуем классический цикл по числовому индексу от 0 до КоличествоСтрок - 1. При этом мы внедрим проверку, чтобы индекс гарантированно не превысил размерность нашего массива с распределенными суммами.
В формуле расчета СтрокаТЧ.Сумма * Курс / Кратность присутствует деление на кратность валюты. Если в базе данных кратность валюты заполнена некорректно (равна 0), система выдаст критическую ошибку. Мы должны предопределить кратность безопасным образом.
Давайте объединим все наши улучшения в один красивый, чистый и производительный блок кода на языке 1С:
Если СуммаРаспределения <> Неопределено Тогда
// 1. Получаем курс валюты ОДИН раз до начала цикла (оптимизация работы с БД)
КурсДокумента = МодульВалютногоУчета.ПолучитьКурсВалюты(ВалютаДокумента, ЭтотОбъект.Дата);
// Проверяем, что структура курса успешно получена
Если КурсДокумента = Неопределено Тогда
ВызватьИсключение "Не удалось получить курс валюты на дату документа!";
КонецЕсли;
// Извлекаем показатели курса и защищаем алгоритм от деления на ноль
КурсЧисло = КурсДокумента.Курс;
КратностьЧисло = ?(КурсДокумента.Кратность = 0, 1, КурсДокумента.Кратность);
КоличествоСтрок = ЭтотОбъект.Товары.Количество();
КоличествоЭлементовМассива = СуммаРаспределения.Количество();
// 2. Запускаем ОДИН безопасный цикл по индексу строк ТЧ (сложность O(N))
Для i = 0 По КоличествоСтрок - 1 Цикл
СтрокаТЧ = ЭтотОбъект.Товары[i];
// Безопасное получение элемента массива (защита от выхода за границы)
Если i < КоличествоЭлементовМассива Тогда
ЭлементМассива = СуммаРаспределения[i];
Иначе
// Если массив оказался меньше ТЧ, присваиваем 0 или пишем предупреждение в лог
ЭлементМассива = 0;
КонецЕсли;
// 3. Формируем проводку бухучета
Проводка = Проводки.Добавить();
Проводка.Период = ЭтотОбъект.Дата;
Проводка.Организация = ЭтотОбъект.Организация;
Проводка.СчетДт = Счет15_02;
// Вычисляем сумму проводки с округлением до 4 знаков
СуммаВВалютеРегл = СтрокаТЧ.Сумма * КурсЧисло / КратностьЧисло;
Проводка.Сумма = Окр(СуммаВВалютеРегл, 4) - ЭлементМассива;
КонецЦикла;
КонецЕсли;
Мы проанализировали типичную проблему неоптимального кода и успешно реорганизовали алгоритм. Избавившись от вложенного цикла, мы ускорили выполнение операции в сотни раз для больших документов. Вынесение запроса курса валюты за пределы цикла сняло лишнюю нагрузку с сервера СУБД, а проверка индексов и деления на ноль сделала код устойчивым к любым непредвиденным действиям пользователя или сбоям в заполнении справочников валют.