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