Как правильно добавлять строки в табличную часть в цикле 1С и избежать бесконечного зацикливания?

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

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

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

Выясняем причину: почему происходит бесконечный цикл?

Давайте посмотрим на классический ошибочный код, который часто пишут начинающие разработчики:


Для каждого Стр Из Объект.Информация Цикл
    // ...какие-то вычисления...
    НоваяСтрока = Объект.Информация.Добавить();
    // ...заполнение полей новой строки...
КонецЦикла;

Проанализируем эту ситуацию с точки зрения работы платформы 1С. Конструкция Для каждого ... Из ... Цикл использует внутренний итератор для последовательного обхода элементов живой коллекции. Когда мы вызываем метод Добавить() непосредственно для обходимой коллекции Объект.Информация, платформа динамически увеличивает размер этой коллекции.

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

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

---

Решение 1. Предварительная выгрузка табличной части в Таблицу значений (Рекомендуемый способ)

Этот способ является самым простым, надежным и красивым с точки зрения стандартов разработки фирмы «1С». Идея заключается в том, чтобы сделать моментальный снимок (копию) исходных данных, зафиксировать его в памяти и спокойно обходить его, не боясь динамического изменения структуры.

Рассмотрим по шагам, как это реализовать:

  1. Выгружаем текущую табличную часть во временную Таблицу значений (ТЗ) с помощью метода Выгрузить().
  2. Запускаем обход полученной таблицы значений с помощью цикла Для каждого. Так как эта таблица находится в памяти как отдельный объект и мы ее не модифицируем, цикл гарантированно завершится.
  3. Внутри цикла добавляем новые строки непосредственно в реальную табличную часть объекта (которая на форме или в базе данных).

Посмотрим на пример практической реализации этого подхода:


// Шаг 1. Выгружаем исходные данные во временную таблицу значений
ТаблицаДанных = Объект.Информация.Выгрузить();

а = 1;
// Шаг 2. Обходим зафиксированные данные в памяти
Для каждого Стр Из ТаблицаДанных Цикл
    ТекущийМесяц = Период.ДатаНачала;
    
    Пока ТекущийМесяц < Период.ДатаОкончания Цикл
        // Шаг 3. Добавляем новые строки в исходную табличную часть объекта
        НоваяСтрока = Объект.Информация.Добавить();
        НоваяСтрока.Период = ТекущийМесяц;
        
        // Динамическое обращение к колонкам исходной строки
        НоваяСтрока.Реализация = Стр["Реализация" + а];
        НоваяСтрока.ЗаменаБрака = Стр["ЗаменаБрака" + а];
        
        ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр, "КодSL,Признак");
        
        ТекущийМесяц = ДобавитьМесяц(ТекущийМесяц, 1);
        а = а + 1;
    КонецЦикла;
КонецЦикла;

Этот метод гарантирует абсолютную безопасность. Изменения в коллекции Объект.Информация никак не влияют на итератор, который обходит ТаблицаДанных.

---

Решение 2. Обход коллекции по фиксированному индексу

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

Давайте разберем этот механизм подробнее. Конструкция цикла с явным указанием границ имеет следующий синтаксис:


Для Счетчик = НачальноеЗначение По КонечноеЗначение Цикл
    // Код цикла
КонецЦикла;

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

Посмотрим на пример реализации кода с использованием этого подхода:


// Фиксируем количество строк ДО начала работы цикла
ПределЦикла = Объект.Информация.Количество() - 1;
а = 1;

Для Инд = 0 По ПределЦикла Цикл
    // Получаем строку по ее индексу
    Стр = Объект.Информация[Инд];
    
    ТекущийМесяц = Период.ДатаНачала;
    Пока ТекущийМесяц < Период.ДатаОкончания Цикл
        
        // Добавление строки безопасно, так как ПределЦикла уже вычислен
        НоваяСтрока = Объект.Информация.Добавить();
        НоваяСтрока.Период = ТекущийМесяц;
        НоваяСтрока.Реализация = Стр["Реализация" + а];
        НоваяСтрока.ЗаменаБрака = Стр["ЗаменаБрака" + а];
        
        ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр, "КодSL,Признак");
        
        ТекущийМесяц = ДобавитьМесяц(ТекущийМесяц, 1);
        а = а + 1;
    КонецЦикла;
КонецЦикла;

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

---

Важный нюанс: Ошибка контекста управляемой формы («Объект» vs «ТекущийОбъект»)

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

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


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    // ОШИБКА: Изменения вносятся в реквизит формы Объект!
    Для Каждого Стр Из Объект.Информация Цикл
         НоваяСтрока = Объект.Информация.Добавить(); 
         // ...
    КонецЦикла;
КонецПроцедуры

Почему это не работает? В момент записи система берет данные из реквизита формы Объект, конвертирует их в прикладной объект базы данных (тип ДокументОбъект или СправочникОбъект) и передает его в процедуру под именем ТекущийОбъект.

Если внутри этой процедуры вы будете изменять реквизит формы Объект, то эти изменения просто проигнорируются! Записываться в базу будет именно ТекущийОбъект. После завершения записи данные из сохраненного ТекущегоОбъекта вернутся на клиент и перезапишут реквизит формы, стерев все ваши программные изменения в Объект.Информация.

Правильный вариант работы в обработчике записи выглядит так:


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    
    // Выгружаем данные из реквизита формы во временную ТЗ
    ТаблицаДляОбхода = ИнформацияФормы.Выгрузить();
    
    Для Каждого Стр Из ТаблицаДляОбхода Цикл
        а = 1;
        ТекущийМесяц = Период.ДатаНачала;
        
        Пока ТекущийМесяц < Период.ДатаОкончания Цикл
            
            // Внимание: добавляем строку в ТЕКУЩИЙ ОБЪЕКТ, который идет на запись
            НоваяСтрока = ТекущийОбъект.Информация.Добавить();
            НоваяСтрока.Период = ТекущийМесяц;
            
            НоваяСтрока.Реализация = Стр["Реализация" + а];
            НоваяСтрока.ЗаменаБрака = Стр["ЗаменаБрака" + а];
            
            ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр, "КодSL, Признак");
            
            ТекущийМесяц = ДобавитьМесяц(ТекущийМесяц, 1);
            а = а + 1;
        КонецЦикла;
    КонецЦикла;
    
КонецПроцедуры

---

Архитектурный совет по улучшению структуры данных

Обратите внимание на фрагмент кода Стр["Реализация" + а]. Подобное динамическое обращение к колонкам по их текстовому имени (когда на форме заготовлены колонки вида Реализация1, Реализация2, Реализация3 и т.д.) является признаком плохой архитектуры решения.

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

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

← На главную