Как правильно удалять элементы из коллекции в цикле в 1С?

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

При разработке на платформе «1С:Предприятие» программисты часто сталкиваются с необходимостью обойти некоторую коллекцию значений (это может быть ТаблицаЗначений, СписокЗначений, Массив, ДеревоЗначений или табличная часть документа) и удалить из неё определенные элементы по условию. Однако стандартный и интуитивно понятный обход с помощью цикла Для каждого в данном случае приводит к логической ошибке: часть элементов просто пропускается и не обрабатывается.

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

Выясняем причину: почему при обычном цикле удаляется только половина элементов?

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


Для каждого ЦиклТекСтр из ПолеСпискаОткуда Цикл
    ПолеСпискаКуда.Добавить(ЦиклТекСтр.Значение, ЦиклТекСтр.Представление, ЦиклТекСтр.Пометка, ЦиклТекСтр.Картинка);
    ПолеСпискаОткуда.Удалить(ЦиклТекСтр);    
КонецЦикла;

На первый взгляд логика кажется верной, но при запуске из 184 элементов переносится ровно 92 (ровно половина), при повторном запуске — 46, затем 23 и так далее. Почему так происходит?

Дело в том, что цикл Для каждого работает на основе внутреннего итератора (в контексте платформы это связано с интерфейсом COMs-коллекций IEnumVariant), который отслеживает текущую позицию (индекс) обхода от начала коллекции. Когда мы вызываем метод Удалить() для текущего элемента, происходит следующее:

  1. Текущий элемент удаляется из коллекции.
  2. Все элементы, находившиеся в коллекции после удаленного, автоматически сдвигаются на одну позицию влево (их индексы уменьшаются на единицу).
  3. На следующей итерации цикла внутренний счетчик (указатель) переходит к следующему индексу (например, от индекса 0 к индексу 1).
  4. Но так как элементы сдвинулись влево, элемент, который ранее находился на позиции 1, теперь занял позицию 0. А на позиции 1 оказался элемент, который изначально был вторым. В результате элемент, переместившийся на позицию 0, просто «перепрыгивается» циклом и остается необработанным.

Проанализируем простую абстрактную ситуацию. Допустим, в коллекции Массив у нас есть три элемента: [Элемент_A, Элемент_B, Элемент_C] с индексами 0, 1 и 2 соответственно.

Шаг 1. Итератор находится на позиции 0. Мы обрабатываем Элемент_A и удаляем его. Коллекция сжимается, и теперь она выглядит так: [Элемент_B, Элемент_C]. При этом Элемент_B теперь имеет индекс 0, а Элемент_C — индекс 1.

Шаг 2. Переходим к следующей итерации. Счетчик цикла увеличивается и указывает на индекс 1. Но под индексом 1 в нашей измененной коллекции теперь находится Элемент_C! Мы обрабатываем его и удаляем. Элемент_B, который сместился на индекс 0, был полностью пропущен алгоритмом обхода.

Именно из-за этого постоянного сдвига индексов при прямом обходе удаляется ровно каждый второй элемент коллекции.

Способ 1. Обратный обход коллекции (классический метод)

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

Для реализации обратного обхода мы не можем использовать цикл Для каждого, так как он всегда идет от начала к концу. Вместо этого мы используем цикл Пока или цикл Для с вычислением индекса.

Рассмотрим наиболее читаемый и надежный вариант с циклом Пока:


Индекс = ПолеСпискаОткуда.Количество() - 1;
Пока Индекс >= 0 Цикл
    ТекущийЭлемент = ПолеСпискаОткуда[Индекс];
    
    // Переносим данные в целевую коллекцию
    ПолеСпискаКуда.Добавить(ТекущийЭлемент.Значение, ТекущийЭлемент.Представление, ТекущийЭлемент.Пометка, ТекущийЭлемент.Картинка);
    
    // Безопасно удаляем по индексу
    ПолеСпискаОткуда.Удалить(Индекс);
    
    // Двигаемся к началу коллекции
    Индекс = Индекс - 1;
КонецЦикла;

Также мы можем реализовать этот алгоритм с помощью цикла Для, вычитая текущий шаг из максимального индекса:


ВсегоЭлементов = ПолеСпискаОткуда.Количество();
Для Шаг = 0 По ВсегоЭлементов - 1 Цикл
    Индекс = ВсегоЭлементов - 1 - Шаг;
    ТекущийЭлемент = ПолеСпискаОткуда[Индекс];
    
    ПолеСпискаКуда.Добавить(ТекущийЭлемент.Значение, ТекущийЭлемент.Представление, ТекущийЭлемент.Пометка, ТекущийЭлемент.Картинка);
    ПолеСпискаОткуда.Удалить(Индекс);
КонецЦикла;

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

Способ 2. Двухпроходной метод (использование временного массива)

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

Разберем этот алгоритм по шагам:

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

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


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

// Первый проход: анализируем коллекцию и собираем элементы для удаления
Для каждого ЭлементСписка Из ПолеСпискаОткуда Цикл
    // Копируем данные в другую коллекцию
    ПолеСпискаКуда.Добавить(ЭлементСписка.Значение, ЭлементСписка.Представление, ЭлементСписка.Пометка, ЭлементСписка.Картинка);
    
    // Запоминаем, что этот элемент подлежит удалению
    МассивУдаляемых.Добавить(ЭлементСписка);
КонецЦикла;

// Второй проход: безопасно удаляем элементы из исходной коллекции
Для каждого ЭлементДляУдаления Из МассивУдаляемых Цикл
    ПолеСпискаОткуда.Удалить(ЭлементДляУдаления);
КонецЦикла;

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

Способ 3. Платформенные оптимизации (когда циклы не нужны)

Очень часто программисты используют циклы для удаления элементов там, где можно обойтись встроенными возможностями платформы «1С:Предприятие». Программное ядро 1С написано на языке C++, и любые встроенные методы коллекций отрабатывают в десятки раз быстрее, чем аналогичные циклы, написанные на встроенном языке 1С.

Проанализируем альтернативные варианты оптимизации кода:

  1. Полная очистка коллекции. Если по условиям задачи вам требуется перенести абсолютно все элементы из одного списка в другой, а исходный полностью очистить (как в примере автора темы), то построчное удаление не имеет смысла. Гораздо эффективнее выполнить перенос элементов, а затем очистить исходную коллекцию одним вызовом метода Очистить():
    
    Для каждого Элемент Из ПолеСпискаОткуда Цикл
        ПолеСпискаКуда.Добавить(Элемент.Значение, Элемент.Представление, Элемент.Пометка, Элемент.Картинка);
    КонецЦикла;
    
    // Очищаем коллекцию мгновенно одной командой
    ПолеСпискаОткуда.Очистить();
    
  2. Выборочное удаление по условию (метод НайтиСтроки). Если мы работаем с объектом типа ТаблицаЗначений или табличной частью документа, и нам нужно удалить строки по определенному значению колонки, мы можем воспользоваться встроенным методом НайтиСтроки(). Он возвращает массив строк, соответствующий отбору. Удаление элементов массива не влияет на структуру самого массива, поэтому мы можем безопасно удалить их из таблицы:
    
    ПараметрыОтбора = Новый Структура("Статус", Перечисления.СтатусыДокументов.Неактивен);
    НайденныеСтроки = ТаблицаДокументов.НайтиСтроки(ПараметрыОтбора);
    
    Для каждого СтрокаДляУдаления Из НайденныеСтроки Цикл
        ТаблицаДокументов.Удалить(СтрокаДляУдаления);
    КонецЦикла;
    
  3. Метод копирования вместо удаления. Если перед вами стоит задача отфильтровать огромную таблицу значений (например, содержащую более 50 000 строк) и удалить из неё значительную часть данных, то поштучное удаление строк будет работать крайне медленно. В этом случае гораздо быстрее скопировать нужные строки в новую таблицу с помощью метода Скопировать(), указав структуру отбора для тех элементов, которые нужно оставить:
    
    // Вместо удаления ненужных строк, копируем только те, которые должны остаться
    ПараметрыОтбора = Новый Структура("Актуально", Истина);
    ТаблицаАктуальныхДанных = ИсходнаяТаблица.Скопировать(ПараметрыОтбора);
    

Оценка производительности алгоритмов (Теория сложности $O(N)$)

С точки зрения теории алгоритмов, выбор способа удаления элементов напрямую влияет на скорость работы системы при масштабировании данных:

Современные стандарты разработки 1С и автоматический контроль качества

В современной промышленной разработке на платформе «1С:Предприятие 8» ручной поиск таких ошибок уходит в прошлое. Для предотвращения попадания подобного некачественного кода в рабочие базы данных (продуктив) используются специализированные инструменты статического анализа кода:

В этих системах настроено строгое правило проверки под названием DeletingCollectionItem (или «Удаление элемента при обходе коллекции посредством оператора Для каждого»). Попытка написать код с удалением элементов внутри прямого цикла Для каждого автоматически распознается этими системами как критическая ошибка уровня Error. Проект с такой ошибкой не пройдет автоматический аудит и сборку (CI/CD) и будет отклонен на этапе коммита.

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

← На главную