Многие начинающие разработчики сталкиваются с ситуацией, которая на первый взгляд кажется мистической: при попытке удалить строки из табличной части или таблицы значений в цикле Для каждого программа обрабатывает ровно половину записей. Если в таблице было 8 строк, удаляются 4. Если запустить код еще раз — удалятся 2 из оставшихся. Кажется, что платформа ведет себя непредсказуемо, но на самом деле мы имеем дело с классической логической ловушкой, связанной с механизмом работы итераторов в 1С.
В этой статье мы подробно разберем, почему так происходит, проанализируем внутренние механизмы платформы и рассмотрим несколько надежных способов решения этой задачи, которые позволят вам корректно очищать коллекции данных.
Разберем ситуацию на примере. Когда мы используем конструкцию Для Каждого Строка Из Коллекция Цикл, платформа 1С инициализирует внутренний указатель (счетчик), который изначально указывает на первую строку (индекс 0). На следующей итерации счетчик увеличивается и указывает на индекс 1, затем на индекс 2 и так далее.
Представим, что мы находимся на первой итерации (индекс 0) и выполняем команду Коллекция.Удалить(Строка). В этот момент происходит следующее:
Проблема заключается в том, что на позиции с индексом 1 теперь находится бывшая строка №2. А та строка, которая была под номером 1 и переместилась на индекс 0, просто «пролетает» мимо проверки, так как счетчик ее уже проскочил. Именно поэтому цикл проходит через одну строку, и в итоге обрабатывается лишь половина коллекции.
Проанализируем самый эффективный и лаконичный способ решения проблемы — обход коллекции в обратном порядке. Если мы начнем удаление с последней строки и будем двигаться к первой, то удаление элемента никак не повлияет на индексы тех строк, которые нам еще только предстоит обработать.
Рассмотрим пример кода, реализующий этот подход:
// Определяем количество строк в табличной части
ВсегоСтрок = ТЧДок.Количество();
// Запускаем цикл от последнего индекса к первому (шаг -1)
Для Индекс = 1 По ВсегоСтрок Цикл
ТекущийИндекс = ВсегоСтрок - Индекс;
ТекОтчет = ТЧДок[ТекущийИндекс];
// Проверяем условие удаления
Если ТекОтчет.ДатаОтчетаПлановая >= ПР_РасчитатьПланОтгрузкиДо Тогда
ТЧДок.Удалить(ТекОтчет);
КонецЕсли;
КонецЦикла;
В данном случае, даже если мы удалим строку с индексом 5, строки с индексами 4, 3, 2, 1 и 0 останутся на своих местах. Их индексы не изменятся, и цикл корректно проверит каждую из них. Это самый надежный метод, не требующий выделения дополнительной памяти под вспомогательные коллекции.
Если вам привычнее работать с циклом Для каждого, можно воспользоваться методом предварительного отбора. Суть заключается в том, чтобы сначала собрать все объекты, подлежащие удалению, в отдельный массив, а затем отдельным циклом удалить их из основной таблицы.
Посмотрим на пример реализации:
// Создаем массив для хранения ссылок на строки, которые нужно удалить
МассивКУдалению = Новый Массив;
// Первый проход: только поиск и фиксация
Для Каждого ТекОтчет Из ТЧДок Цикл
Если ТекОтчет.ДатаОтчетаПлановая >= ПР_РасчитатьПланОтгрузкиДо Тогда
МассивКУдалению.Добавить(ТекОтчет);
КонецЕсли;
КонецЦикла;
// Второй проход: непосредственное удаление
Для Каждого СтрокаДляУдаления Из МассивКУдалению Цикл
ТЧДок.Удалить(СтрокаДляУдаления);
КонецЦикла;
Преимущество этого метода в его читаемости. Мы четко разделяем логику поиска данных и логику модификации коллекции. Это особенно полезно, если условие удаления очень сложное или требует обращения к серверу. Поскольку массив хранит прямые ссылки на строки табличной части, метод Удалить() отработает корректно, даже если физический индекс строки в таблице изменился.
Этот способ позволяет избежать создания лишних объектов (как в случае с массивом) и при этом двигаться «сверху вниз». Мы будем использовать цикл Пока и самостоятельно решать, когда нужно переходить к следующему индексу.
Проанализируем логику:
Индекс = 0;
Пока Индекс < ТЧДок.Количество() Цикл
ТекОтчет = ТЧДок[Индекс];
Если ТекОтчет.ДатаОтчетаПлановая >= ПР_РасчитатьПланОтгрузкиДо Тогда
// Если удаляем строку, индекс НЕ увеличиваем,
// так как следующая строка сама «прыгнет» на текущий индекс
ТЧДок.Удалить(ТекОтчет);
Иначе
// Если строка остается, переходим к следующей
Индекс = Индекс + 1;
КонецЕсли;
КонецЦикла;
Этот метод считается очень гибким. Обратите внимание, что мы проверяем ТЧДок.Количество() на каждой итерации, так как размер коллекции постоянно уменьшается.
Если ваше условие удаления заключается в простом поиске по равенству значений (например, удалить все строки, где Статус = "Отменено"), лучше всего воспользоваться встроенным методом НайтиСтроки(). Он возвращает массив строк, соответствующих структуре поиска.
Посмотрим на пример:
ПараметрыОтбора = Новый Структура("Статус", Перечисления.СтатусыЗаказов.Отменен);
СтрокиКУдалению = ТЧДок.НайтиСтроки(ПараметрыОтбора);
Для Каждого Строка Из СтрокиКУдалению Цикл
ТЧДок.Удалить(Строка);
КонецЦикла;
Этот метод работает быстрее, чем ручной перебор всей таблицы, так как поиск оптимизирован на уровне платформы.
Рассмотрим важный аспект: работа с большими объемами данных. Если в вашей табличной части тысячи строк и вам нужно удалить значительную их часть (например, более 50%), удаление в цикле может работать медленно. Это связано с тем, что каждое удаление вызывает пересчет индексов коллекции и, в случае работы с формой, может провоцировать обновление интерфейса.
В таких случаях рекомендуется:
ТаблицуЗначений методом Выгрузить().ТаблицуЗначений, скопировав в нее только те строки, которые должны остаться (используя метод Скопировать() с фильтром или перебором).Очистить().Загрузить().Этот подход часто оказывается на порядок быстрее при массовых манипуляциях с данными, так как минимизирует количество транзакционных действий с объектом базы данных.
Подводя итог, можно сказать, что «чудес» в 1С не бывает: если цикл пропускает строки, значит, коллекция меняется прямо во время обхода. Используйте удаление с конца или предварительный отбор в массив, и ваш код всегда будет работать предсказуемо и корректно.