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