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