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

Программист 1С v8.3 (Управляемые формы) 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

При разработке механизмов распределения сумм, затрат или скидок в 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. Риск выхода за границы массива (ошибка «Индекс находится за границами диапазона»): Если логика расчета массива СуммаРаспределения по какой-то причине вернет меньше элементов, чем строк в табличной части (например, из-за округления или исключения нулевых строк), программа аварийно завершит работу с ошибкой у пользователя.
  2. Ненадежность реквизита НомерСтроки: Номер строки жестко привязан к физическому порядку строк в табличной части на форме. Если перед выполнением этого кода табличная часть была отсортирована пользователем, скопирована или из нее были программно удалены строки, синхронизация между индексом массива и номером строки нарушится. Это приведет к тому, что суммы распределятся не на те товары.
  3. Критический «киллер» производительности внутри цикла: Строка получения курса валюты МодульВалютногоУчета.ПолучитьКурсВалюты(ВалютаДокумента, Дата) осталась внутри цикла. Метод получения курса делает запрос к базе данных (к регистру сведений). Запрос в цикле — это грубейшее нарушение стандартов «1С». Если в документе 500 строк, система сделает 500 отдельных запросов к СУБД, что вызовет сильные задержки (блокировки базы данных).

Разрабатываем надежное и быстрое решение по шагам

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

Шаг 1: Оптимизируем работу с базой данных (выносим запросы из цикла)

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

Шаг 2: Реализуем безопасный параллельный обход по индексу

Вместо обхода коллекции через Для Каждого и привязки к свойству НомерСтроки, мы организуем классический цикл по числовому индексу от 0 до КоличествоСтрок - 1. При этом мы внедрим проверку, чтобы индекс гарантированно не превысил размерность нашего массива с распределенными суммами.

Шаг 3: Добавляем защиту от деления на ноль

В формуле расчета СтрокаТЧ.Сумма * Курс / Кратность присутствует деление на кратность валюты. Если в базе данных кратность валюты заполнена некорректно (равна 0), система выдаст критическую ошибку. Мы должны предопределить кратность безопасным образом.

Оптимальный шаблон готового кода

Давайте объединим все наши улучшения в один красивый, чистый и производительный блок кода на языке 1С:


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

Резюме

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

← На главную