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