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