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