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