Как удалить строки из дерева значений 1С по условию 'Количество равно нулю'?

Программист 1С v8.3 (Управляемые формы)
← На главную

При работе с объектом ДеревоЗначений в 1С часто возникает задача удаления строк, которые не соответствуют определенным условиям. Одним из таких условий может быть нулевое значение в колонке «Количество». Однако прямое удаление строк во время обхода дерева может привести к непредсказуемому поведению, ошибкам «индекс выходит за пределы» или некорректной очистке всего дерева.

В этой статье мы подробно рассмотрим, почему возникают такие проблемы и предложим несколько надежных способов удаления строк из ДеревоЗначений, включая рекомендации по работе с иерархическими структурами.

Почему возникают проблемы при удалении строк из дерева значений?

Когда мы пытаемся удалить строки из коллекции (будь то ТаблицаЗначений или ДеревоЗначений) во время ее непосредственного обхода, возникают следующие трудности:

  1. Изменение структуры коллекции: При удалении элемента коллекция изменяет свой размер и, возможно, индексы оставшихся элементов. Если обход ведется по индексу (например, Для Инд = 0 По Коллекция.Количество - 1 Цикл ... Коллекция.Удалить(Инд)), то после удаления элемента индексы «сдвигаются», и следующая итерация может пропустить элемент или обратиться к несуществующему индексу.
  2. Иерархическое удаление: В случае с ДеревоЗначений удаление родительской строки автоматически приводит к удалению всех ее дочерних строк. Если наш список строк для удаления содержит как родителя, так и его потомков, и родитель удаляется первым, то последующая попытка удалить уже несуществующего потомка приведет к ошибке, такой как «индекс выходит за пределы» или «объект не найден».
  3. Потеря ссылки на объект: Хотя метод Удалить() может принимать ссылку на объект СтрокаДереваЗначений, это не гарантирует, что сам объект остается валидным в дереве на момент вызова метода, если он был удален как потомок ранее удаленной родительской строки.

Автор исходной проблемы столкнулся именно с этими трудностями, когда его код пытался удалить строки с количеством, равным нулю:


ОтборСтрокКУдалению = Новый Структура("Количество", 0);
МассивСтрокКУдалению = ДеревоОстатков.Строки.НайтиСтроки(ОтборСтрокКУдалению, Истина);

Для Каждого СтрокаДерева Из МассивСтрокКУдалению Цикл
    Попытка
        СтрокиДерева = ?(СтрокаДерева.Родитель = Неопределено, ДеревоОстатков.Строки, СтрокаДерева.Родитель.Строки);
        СтрокиДерева.Удалить(СтрокаДерева);
    Исключение
        // Исключение не должно быть частью нормального алгоритма, но может помочь в отладке.
        // СообщитьОписаниеОшибки(); // Добавление этой строки помогло бы выявить "индекс выходит за пределы"
    КонецПопытки;
КонецЦикла;

Этот код не учитывает порядок удаления элементов в иерархии, что приводит к ошибкам, если в МассивСтрокКУдалению сначала попадает родительская строка с нулевым количеством, а затем ее дочерняя строка, которая уже была удалена вместе с родителем.

Подход 1: Удаление с помощью временной таблицы и сортировки по уровню (наиболее надежный)

Этот метод является самым надежным и рекомендованным для удаления строк из иерархического ДеревоЗначений. Его суть заключается в следующем:

  1. Сбор строк для удаления: Сначала мы идентифицируем все строки, подлежащие удалению, и собираем их во временную коллекцию (например, ТаблицаЗначений или Массив). Важно сохранить не только сами ссылки на строки, но и информацию об их уровне вложенности и родительской коллекции строк.
  2. Сортировка коллекции для удаления: Затем мы сортируем эту временную коллекцию в порядке убывания уровня вложенности. Это гарантирует, что сначала будут обработаны строки самого глубокого уровня (листья), затем их родители и так далее до корневых элементов. Такой порядок исключает попытку удаления уже несуществующих дочерних элементов.
  3. Последовательное удаление: Наконец, мы проходим по отсортированной коллекции и удаляем каждую строку из соответствующей родительской коллекции.

Рассмотрим подробнее реализацию этого подхода:


// Шаг 1: Идентифицируем и собираем строки для удаления.
// Используем метод НайтиСтроки для быстрого поиска всех строк, удовлетворяющих условию.
ОтборСтрокКУдалению = Новый Структура("Количество", 0);
МассивСтрокКУдалению = ДеревоОстатков.Строки.НайтиСтроки(ОтборСтрокКУдалению, Истина);

// Создаем временную ТаблицуЗначений для хранения информации о строках, подлежащих удалению.
// Нам понадобится сама строка, коллекция, из которой ее нужно удалить, и ее уровень.
ТаблицаСтрокДляУдаления = Новый ТаблицаЗначений;
ТаблицаСтрокДляУдаления.Колонки.Добавить("КоллекцияСтрокДереваДляУдаления"); // Родительская коллекция (СтрокиДереваЗначений)
ТаблицаСтрокДляУдаления.Колонки.Добавить("СтрокаДереваДляУдаления");       // Ссылка на удаляемую строку
ТаблицаСтрокДляУдаления.Колонки.Добавить("УровеньСтрокиДереваДляУдаления"); // Уровень вложенности строки

// Заполняем временную таблицу, собирая информацию обо всех найденных строках.
Для Каждого СтрокаДерева Из МассивСтрокКУдалению Цикл
    НС = ТаблицаСтрокДляУдаления.Добавить();
    // Определяем, из какой коллекции нужно удалить строку: из корневой или из коллекции потомков родителя.
    НС.КоллекцияСтрокДереваДляУдаления = ?(СтрокаДерева.Родитель = Неопределено, ДеревоОстатков.Строки, СтрокаДерева.Родитель.Строки);
    НС.СтрокаДереваДляУдаления = СтрокаДерева;
    НС.УровеньСтрокиДереваДляУдаления = СтрокаДерева.Уровень(); // Получаем уровень вложенности строки
КонецЦикла;

// Шаг 2: Сортируем временную таблицу по уровню вложенности в убывающем порядке.
// Это критически важно, чтобы сначала удалить дочерние элементы, а затем их родителей.
ТаблицаСтрокДляУдаления.Сортировать("УровеньСтрокиДереваДляУдаления Убыв");

// Шаг 3: Выполняем удаление строк из дерева.
Для Каждого Стр Из ТаблицаСтрокДляУдаления Цикл
    // Используем метод Удалить(), передавая ссылку на объект СтрокаДереваЗначений.
    // Это более устойчиво, чем удаление по индексу.
    Стр.КоллекцияСтрокДереваДляУдаления.Удалить(Стр.СтрокаДереваДляУдаления);
КонецЦикла;

Данный подход обеспечивает корректное удаление строк независимо от их положения в иерархии, поскольку мы сначала удаляем самые глубокие элементы, избегая ошибок, связанных с уже удаленными родителями. Метод Удалить(СтрокаДереваЗначений) работает по ссылке на объект, что позволяет не беспокоиться о сдвиге индексов в коллекции, из которой происходит удаление.

Подход 2: Использование рекурсии с временным массивом для каждого уровня

Рекурсия — это естественный способ обхода древовидных структур. Для удаления строк мы можем модифицировать рекурсивный обход так, чтобы сначала собирать строки для удаления на текущем уровне, а затем удалять их после завершения обхода этого уровня. Этот подход также гарантирует, что мы не будем изменять коллекцию во время ее обхода.

Рассмотрим пример:


Процедура УдалитьНулевыеВетки(ДеревоКоллекцияСтрок)
    МассивУдаляемыхСтрок = Новый Массив; // Временный массив для сбора строк на текущем уровне

    // Сначала обходим все строки текущего уровня
    Для Каждого СтрокаДерева Из ДеревоКоллекцияСтрок.Строки Цикл
        Если СтрокаДерева.Количество = 0 Тогда
            // Если количество равно 0, добавляем строку в массив для удаления.
            МассивУдаляемыхСтрок.Добавить(СтрокаДерева);
        Иначе
            // Если количество не 0, и у строки есть потомки, рекурсивно вызываем функцию для потомков.
            Если СтрокаДерева.Строки.Количество > 0 Тогда
                УдалитьНулевыеВетки(СтрокаДерева); // Передаем саму строку, чтобы работать с ее коллекцией потомков
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;

    // После того как обход текущего уровня завершен и все потомки обработаны,
    // удаляем собранные строки с текущего уровня.
    Для Каждого УдаляемаяСтрока Из МассивУдаляемыхСтрок Цикл
        ДеревоКоллекцияСтрок.Строки.Удалить(УдаляемаяСтрока);
    КонецЦикла;
КонецПроцедуры

// Инициализация вызова для корневого уровня дерева
// УдалитьНулевыеВетки(ДеревоОстатков); // Предполагается, что ДеревоОстатков - это само ДеревоЗначений

В этом коде процедура УдалитьНулевыеВетки принимает в качестве параметра либо объект ДеревоЗначений (для корневого уровня), либо объект СтрокаДереваЗначений (для вложенных уровней), чтобы получить доступ к коллекции его дочерних строк. Сначала мы рекурсивно обходим все дочерние ветки, собирая строки для удаления. Только после того, как все потомки текущей ветки обработаны, мы удаляем строки из самой текущей ветки. Это также гарантирует, что мы не удалим родителя до того, как его потомки будут проанализированы или удалены.

Производительность рекурсивного подхода: Автор проблемы отмечал, что рекурсивные методы могут работать медленнее для больших деревьев (2-3 секунды против менее 1 секунды для других подходов). Это связано с накладными расходами на вызовы функций и создание множества временных массивов в стеке вызовов. Для деревьев со средней глубиной и небольшим количеством элементов на каждом уровне этот метод может быть приемлемым, но для очень больших и глубоких деревьев стоит рассмотреть первый подход.

Подход 3: Обход с конца для дочерних элементов (для конкретного уровня)

Если задача состоит в удалении строк только из одной конкретной коллекции (например, только из дочерних строк одного родителя или только из корневых строк) и вы работаете по индексу, то классический подход — это обход коллекции в обратном порядке. Это позволяет безопасно удалять элементы по индексу, так как смещение индексов происходит для уже пройденной части коллекции.

Рассмотрим пример, который удаляет «листья» (строки без потомков) из коллекций дочерних строк:


Дерево = РеквизитФормыВЗначение("РаспоряженияНаДоставку"); // Получаем ДеревоЗначений

// Обходим корневые строки
Для Каждого СтрокиПоЗоне Из Дерево.Строки Цикл
    // Получаем количество строк в текущей коллекции потомков
    КоличествоСтрок = СтрокиПоЗоне.Строки.Количество();

    // Обходим коллекцию потомков с конца
    Для й = 1 По КоличествоСтрок Цикл
        Строка = СтрокиПоЗоне.Строки[КоличествоСтрок - й]; // Получаем строку по индексу
        
        // Условие для удаления: например, если Количество равно 0 и нет потомков
        Если Строка.Количество = 0 И Строка.Строки.Количество() = 0 Тогда
            СтрокиПоЗоне.Строки.Удалить(Строка); // Удаляем строку по ссылке
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

Этот метод полезен, если вы точно знаете, из какой коллекции и с какого уровня вы удаляете элементы, и хотите избежать использования временных массивов. Он требует ручного управления индексом или использования метода Удалить(СтрокаДереваЗначений), что более безопасно. Однако для удаления элементов разных уровней иерархии в произвольном порядке он не подходит, и лучше использовать подход с временной таблицей и сортировкой.

Важные моменты и рекомендации

  1. Удаление по ссылке на объект vs. по индексу: Всегда предпочтительнее использовать метод Коллекция.Удалить(СтрокаДереваЗначений), который принимает ссылку на объект, а не Коллекция.Удалить(Индекс). Удаление по ссылке более устойчиво к изменениям индексов в коллекции, пока сам объект СтрокаДереваЗначений остается валидным в дереве. Проблема возникает, когда объект уже был удален как часть более крупной ветки, а код пытается обратиться к нему по старой (хоть и не несуществующей) ссылке.
  2. Диагностика ошибок: При возникновении ошибок типа «индекс выходит за пределы» или падений конфигуратора (как у автора), всегда используйте конструкцию Попытка Исключение Сообщить(ОписаниеОшибки()) КонецПопытки. Это позволит получить детальную информацию о причине сбоя и точно определить место возникновения проблемы.
  3. Оптимизация заполнения дерева: Лучшим решением всегда будет предотвращение проблемы, а не ее исправление. Если это возможно по логике работы программы, рассмотрите изменение алгоритма заполнения ДеревоЗначений таким образом, чтобы строки с нулевым количеством изначально не добавлялись. Это позволит избежать лишних операций по их последующему удалению. Однако, если дерево динамически изменяется в процессе работы, этот вариант может быть неприменим.
  4. Метод НайтиСтроки: Метод ДеревоОстатков.Строки.НайтиСтроки(ОтборСтрокКУдалению, Истина) очень эффективен для быстрого поиска всех строк по заданному условию, включая вложенные уровни. Он возвращает Массив найденных строк, который затем можно использовать для дальнейшей обработки (например, для заполнения временной таблицы для удаления).

Выбор конкретного подхода зависит от размера вашего дерева, глубины иерархии и требований к производительности. Для большинства случаев, особенно с иерархическими структурами, подход с использованием временной таблицы и сортировки по уровню является наиболее универсальным и надежным.

← На главную