Почему при удалении строк из табличной части в цикле часть строк пропускается, а в отладчике всё работает?

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

В практике разработки на платформе 1С часто возникает задача: перебрать строки табличной части документа, проверить условие и, если оно выполняется, удалить строку или перенести её в другую таблицу. Программисты-новички часто сталкиваются с загадочным поведением: код вроде бы написан верно, но после выполнения в таблице остается часть строк, которые должны были быть удалены. Самое странное, что при пошаговой отладке всё может работать корректно, создавая иллюзию "глюка" платформы. Рассмотрим подробнее, почему так происходит и как написать надежный код.

Анализируем причину: механика итератора и смещение индексов

Разберем ситуацию, которая возникла у автора темы. Был использован цикл Для каждого для обхода строк табличной части. Внутри цикла, если номенклатура соответствовала условию, вызывался метод Удалить(). Проанализируем, что происходит в памяти сервера в этот момент:

  1. Платформа начинает обход коллекции. Итератор указывает на строку с индексом 0.
  2. Программа находит совпадение и удаляет строку с индексом 0.
  3. Важный момент: Как только строка удалена, все последующие строки в коллекции автоматически сдвигаются вверх. Строка, которая была под индексом 1, теперь занимает индекс 0. Строка с индексом 2 становится индексом 1 и так далее.
  4. Цикл переходит к следующему шагу. Итератор перемещается к индексу 1.
  5. Но на индексе 1 теперь находится та строка, которая изначально была под индексом 2! Та строка, которая переместилась с индекса 1 на 0, просто пропускается циклом, так как итератор уже "проехал" этот индекс.

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

Выясняем причину "эффекта отладчика"

Многие задаются вопросом: почему под отладкой код внезапно начинает работать? Здесь вступает в силу несколько факторов. Во-первых, при ручном проходе программист часто задерживается на строках, проверяет значения в табло, что может приводить к неявному обновлению контекста объекта. Во-вторых, часто в отладчике проверяется работа на 1-2 документах, где строки для удаления не идут подряд, и ошибка просто не успевает проявиться. Если же идти по коду очень медленно и вручную пересчитывать индексы, станет очевидно, что итератор прыгает через строку.

Решение №1: Обход коллекции с конца (реверсивный цикл)

Самый простой и эффективный способ избежать смещения индексов — обходить таблицу с конца к началу. В этом случае, когда мы удаляем строку с индексом 10, это никак не влияет на позиции строк с индексами 0–9. Рассмотрим пример реализации такого подхода:


КолвоСтрок = ДокОбъект.Товары.Количество();
Для Счетчик = 1 По КолвоСтрок Цикл
    Индекс = КолвоСтрок - Счетчик;
    ТекущаяСтрока = ДокОбъект.Товары[Индекс];
    
    Если НужнаяНоменклатура(ТекущаяСтрока.Номенклатура) Тогда
        // Сначала копируем данные, если это перенос
        НоваяСтрока = ДокОбъект.АгентскиеУслуги.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, ТекущаяСтрока);
        
        // Удаляем строку — индексы строк выше не меняются!
        ДокОбъект.Товары.Удалить(ТекущаяСтрока);
    КонецЕсли;
КонецЦикла;

Здесь мы используем обратный отсчет индекса. Проанализируем ситуацию: если мы удалили последнюю строку, предпоследняя осталась на своем месте, и на следующей итерации мы корректно к ней обратимся.

Решение №2: Использование вспомогательного массива

Если работа с обратными индексами кажется вам запутанной, воспользуемся более современным подходом, рекомендуемым стандартами разработки 1С. Мы сначала соберем все строки, подлежащие удалению, в отдельный массив, а затем пройдем по этому массиву и удалим их из основной таблицы.

Рассмотрим по шагам этот алгоритм:


// Создаем массив для хранения ссылок на строки табличной части
СтрокиДляУдаления = Новый Массив;

// Первый проход: только поиск
Для каждого СтрТовары Из ДокОбъект.Товары Цикл
    Если СтрТовары.Номенклатура.Наименование ПОДОБНО "Полис%" Тогда
        СтрокиДляУдаления.Добавить(СтрТовары);
    КонецЕсли;
КонецЦикла;

// Второй проход: выполнение действий
Для каждого СтрДляУдаления Из СтрокиДляУдаления Цикл
    // Добавляем в другую ТЧ
    НоваяСтрока = ДокОбъект.АгентскиеУслуги.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрДляУдаления);
    
    // Удаляем из исходной ТЧ
    ДокОбъект.Товары.Удалить(СтрДляУдаления);
КонецЦикла;

Данный метод безопасен, так как массив хранит прямые ссылки на объекты строк. Удаление строки из коллекции Товары не портит итератор цикла по массиву СтрокиДляУдаления. Этот код гораздо легче читать и отлаживать.

Оптимизация кода: избавляемся от "говнокода"

В исходном сообщении автора были и другие критические ошибки, которые могут привести к падению производительности системы. Проанализируем их и исправим:

  1. Запросы в цикле: Никогда не делайте Запрос.Выполнить() внутри цикла по документам. Правильно — одним запросом выбрать все нужные данные (ссылки на документы и нужную номенклатуру), а затем обрабатывать результат.
  2. Поиск по коду: Использование НайтиПоКоду("00-000322") — плохая практика. Если код в справочнике изменится или база будет перенесена, обработка перестанет работать. Лучше вынести выбор контрагента и договора в реквизиты формы обработки.
  3. Транзакции: Не нужно явно вызывать НачатьТранзакцию() при записи одного объекта. Метод Записать() сам открывает транзакцию. Лишние вложенные транзакции только усложняют код и могут привести к блокировкам.

Посмотрим на пример оптимизированной процедуры, которая объединяет все исправления:


&НаСервере
Процедура ВыполнитьОбработкуНаСервере()
    
    // Подготавливаем данные заранее (не в цикле!)
    КонтрагентСсылка = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", "7700000000"); 
    ДоговорСсылка = Справочники.ДоговорыКонтрагентов.НайтиПоНаименованию("Основной договор", Ложь, , КонтрагентСсылка);
    
    // Запрос для выбора документов
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	Отчет.Ссылка КАК Ссылка
    |ИЗ
    |	Документ.ОтчетОРозничныхПродажах КАК Отчет
    |ГДЕ
    |	Отчет.Дата МЕЖДУ &ДатаНач И &ДатаКон
    |	И НЕ Отчет.РучнаяКорректировка";
    
    Запрос.УстановитьПараметр("ДатаНач", НачалоДня(Объект.ДатаНачала));
    Запрос.УстановитьПараметр("ДатаКон", КонецДня(Объект.ДатаОкончания));
    
    Результат = Запрос.Выполнить();
    Выборка = Результат.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        ДокОбъект = Выборка.Ссылка.ПолучитьОбъект();
        СтрокиДляПереноса = Новый Массив;
        
        // Отбираем строки
        Для каждого Стр Из ДокОбъект.Товары Цикл
            // Используем Лев() для поиска по началу названия
            Если ВРЕГ(Лев(Стр.Номенклатура.Наименование, 5)) = "ПОЛИС" Тогда
                СтрокиДляПереноса.Добавить(Стр);
            КонецЕсли;
        КонецЦикла;
        
        Если СтрокиДляПереноса.Количество() > 0 Тогда
            
            Для каждого СтрДляУдаления Из СтрокиДляПереноса Цикл
                НоваяСтрока = ДокОбъект.АгентскиеУслуги.Добавить();
                ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрДляУдаления);
                
                // Заполняем специфичные реквизиты
                НоваяСтрока.Контрагент = КонтрагентСсылка;
                НоваяСтрока.ДоговорКонтрагента = ДоговорСсылка;
                НоваяСтрока.СчетРасчетов = ПланыСчетов.Хозрасчетный.ПрочиеРасчетыСРазнымиДебиторамиИКредиторами;
                
                ДокОбъект.Товары.Удалить(СтрДляУдаления);
            КонецЦикла;
            
            Попытка
                ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
            Исключение
                ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Ошибка записи документа: " + Выборка.Ссылка);
            КонецПопытки;
            
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

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

← На главную