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