Работа с деревом значений в 1С:Предприятие — одна из самых востребованных задач при разработке отчетов, обработок и интеграционных решений. Этот объект платформы позволяет хранить иерархические данные, но его обход часто вызывает вопросы даже у опытных программистов. В отличие от обычного таблицы значений, где строки перебираются линейно, дерево требует рекурсивных подходов или использования специализированных методов платформы.

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

1. Базовые методы обхода: цикл Для Каждого и ПолучениеСтрок

Начнем с самых простых способов, которые подходят для большинства задач. Метод ПолучениеСтрок() возвращает коллекцию строк дерева, которую можно перебрать стандартным циклом. Это удобно, когда не нужна рекурсия или обработка вложенных узлов.

Пример кода для линейного обхода:

Дерево = Новый ДеревоЗначений;

Дерево.Колонки.Добавить("Наименование");

Дерево.Колонки.Добавить("Количество");

// Заполняем дерево данными

ЭлементДерева = Дерево.Строки.Добавить();

ЭлементДерева.Наименование = "Узел 1";

ЭлементДерева.Количество = 10;

// Обход всех строк

Для Каждого СтрокаДерева Из Дерево.ПолучениеСтрок() Цикл

Сообщить(СтрокаДерева.Наименование + " - " + СтрокаДерева.Количество);

КонецЦикла;

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

💡

Для ускорения работы с большими деревьями (10 000+ строк) предварительно отключите визуальное отображение дерева методом Дерево.Отображать(Ложь) перед обходом.

  • ✅ Простота реализации — подходит для начинающих
  • ✅ Быстродействие на небольших деревьях (до 1000 строк)
  • ❌ Не работает с иерархией — игнорирует родительские связи
  • ❌ Может тормозить при большом количестве строк

2. Рекурсивный обход: обработка вложенных узлов

Когда необходимо обработать все уровни вложенности дерева, используется рекурсивный обход. Этот метод позволяет "спускаться" по каждому узлу, обрабатывая сначала его самого, а затем всех его потомков. Классическая реализация выглядит так:

Процедура ОбойтиДеревоРекурсивно(Узел)

// Обработка текущего узла

Сообщить(Узел.Наименование);

// Рекурсивный обход дочерних узлов

Если Узел.Родитель Тогда

Для Каждого ДочернийУзел Из Узел.Строки Цикл

ОбойтиДеревоРекурсивно(ДочернийУзел);

КонецЦикла;

КонецЕсли;

КонецПроцедуры

// Вызов для корневых узлов

Для Каждого КорневойУзел Из Дерево.ПолучениеСтрок(,, Истина) Цикл

ОбойтиДеревоРекурсивно(КорневойУзел);

КонецЦикла;

Ключевой момент здесь — проверка Если Узел.Родитель, которая позволяет определить, является ли текущая строка корневой. Параметр Истина в методе ПолучениеСтрок() указывает, что нужно возвращать только корневые узлы.

⚠️ Внимание: При рекурсивном обходе глубоких деревьев (более 10 уровней вложенности) возможен переполнение стека. В таких случаях лучше использовать итеративный подход с явным управлением стека.
📊 Какой метод обхода вы используете чаще?
Рекурсивный
Итеративный со стеком
ПолучениеСтрок()
Специализированные методы платформы

3. Итеративный обход со стеком: альтернатива рекурсии

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

Стек = Новый Массив;

Дерево = Новый ДеревоЗначений;

// Заполняем стек корневыми узлами

Для Каждого КорневойУзел Из Дерево.ПолучениеСтрок(,, Истина) Цикл

Стек.Добавить(КорневойУзел);

КонецЦикла;

Пока Стек.Количество() > 0 Цикл

ТекущийУзел = Стек.Получить(Стек.ВГраница());

// Обработка узла

Сообщить(ТекущийУзел.Наименование);

// Добавляем дочерние узлы в стек (в обратном порядке для правильной обработки)

Если ТекущийУзел.ЕстьСтроки() Тогда

Для Инд = ТекущийУзел.Строки.Количество() - 1 По 0 Шаг -1 Цикл

Стек.Добавить(ТекущийУзел.Строки.Получить(Инд));

КонецЦикла;

КонецЕсли;

КонецЦикла;

Преимущество этого метода — полный контроль над процессом обхода и отсутствие ограничений по глубине вложенности. Однако код становится более сложным для восприятия.

Метод обхода Сложность реализации Макс. глубина вложенности Производительность
Цикл Для Каждого Низкая Не применим Высокая
Рекурсивный Средняя Ограничена стеком (~1000 уровней) Средняя
Итеративный со стеком Высокая Не ограничена Высокая
Выгрузка в таблицу Низкая Не применим Низкая (для больших деревьев)

4. Оптимизация производительности при обходе

При работе с большими деревьями (десятки тысяч строк) стандартные методы обхода могут работать медленно. Вот ключевые приемы оптимизации:

  • 🚀 Отключение визуализации: Дерево.Отображать(Ложь) перед обходом ускоряет работу в 2-3 раза
  • 📊 Выгрузка в таблицу: Для линейной обработки можно выгрузить дерево в таблицу значений методом ВыгрузитьКолонки()
  • 🔍 Индексирование: Если часто ищутся узлы по определенному полю, стоит завести отдельную коллекцию с индексами
  • Пакетная обработка: Для массовых операций используйте НачатьИзменение()/ЗакончитьИзменение()

Критическая ошибка многих разработчиков — обход дерева непосредственно в цикле отображения формы. Это приводит к "подвисанию" интерфейса. Всегда выносите тяжелые операции в фоновые задачи или используйте прогресс-бар:

Прогресс = Новый ПрогрессОперации("Обработка дерева...");

Попытка

Для Каждого Строка Из Дерево.ПолучениеСтрок() Цикл

// Обработка строки

Прогресс.УстановитьПрогресс(Прогресс.ТекущееЗначение + 1);

КонецЦикла;

Исключение

Прогресс.Закрыть();

ВызватьИсключение;

КонецПопытки;

💡

Для деревьев более 50 000 строк оптимально использовать выгрузку в таблицу значений с последующей обработкой стандартными методами таблиц.

5. Работа с иерархией: получение пути к узлу

Часто требуется не просто обойти дерево, но и получить полный путь от корня до текущего узла. Это актуально для построения отчетов или логирования. Реализуется это через рекурсивное формирование пути:

Функция ПолучитьПутьКУзлу(Узел, Разделитель = " > ")

Если Узел.Родитель = Неопределено Тогда

Возврат Узел.Наименование;

Иначе

Возврат ПолучитьПутьКУзлу(Узел.Родитель, Разделитель) + Разделитель + Узел.Наименование;

КонецЕсли;

КонецФункции

Пример использования:

Для Каждого Узел Из Дерево.ПолучениеСтрок() Цикл

Путь = ПолучитьПутьКУзлу(Узел);

Сообщить(Путь);

КонецЦикла;

Для деревьев с большой вложенностью такой подход может быть ресурсоемким. Альтернатива — хранение пути непосредственно в колонках дерева при его формировании.

⚠️ Внимание: При изменении структуры дерева (перемещении узлов) ранее сохраненные пути становятся неактуальными. Всегда пересчитывайте пути динамически или используйте уникальные идентификаторы узлов.

6. Типовые ошибки и их решение

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

  • 🔄 Зацикливание при рекурсии: Возникает если в коде не проверяется условие выхода. Всегда ограничивайте глубину рекурсии или используйте итеративный подход
  • 🗑️ Потеря данных при изменении: Никогда не модифицируйте дерево (добавляйте/удаляйте строки) во время его обхода — это приводит к непредсказуемым результатам
  • 🔍 Неправильная фильтрация: Метод ПолучениеСтрок() с отбором работает только по виртуальным таблицам. Для сложной фильтрации лучше выгрузить данные в таблицу
  • 📈 Проблемы с сортировкой: Деревья не поддерживают автоматическую сортировку. При необходимости сортируйте строки перед добавлением

Типичный пример ошибки — попытка удалить строку во время ее обхода:

// НЕПРАВИЛЬНО!

Для Каждого Строка Из Дерево.ПолучениеСтрок() Цикл

Если Строка.Количество = 0 Тогда

Дерево.Строки.Удалить(Строка); // Приведет к ошибке!

КонецЕсли;

КонецЦикла;

Правильный вариант:

СтрокиДляУдаления = Новый Массив;

Для Каждого Строка Из Дерево.ПолучениеСтрок() Цикл

Если Строка.Количество = 0 Тогда

СтрокиДляУдаления.Добавить(Строка);

КонецЕсли;

КонецЦикла;

Для Каждого Строка Из СтрокиДляУдаления Цикл

Дерево.Строки.Удалить(Строка);

КонецЦикла;

7. Продвинутые техники: обход с учетом уровней

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

Процедура ОбойтиСУровнями(Узел, Уровень = 0)

Отступ = СтрПовтор(" ", Уровень);

Сообщить(Отступ + Узел.Наименование + " (Уровень: " + Уровень + ")");

Если Узел.ЕстьСтроки() Тогда

Для Каждого ДочернийУзел Из Узел.Строки Цикл

ОбойтиСУровнями(ДочернийУзел, Уровень + 1);

КонецЦикла;

КонецЕсли;

КонецПроцедуры

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

Как рассчитать сумму по всем уровнем дерева?

Для расчета иерархических сумм (например, "итого по подразделению") используйте рекурсивную функцию, которая возвращает сумму текущего узла плюс суммы всех дочерних узлов. Пример кода доступен по запросу в комментариях.

Еще один полезный прием — построение плоской таблицы с учетом иерархии. Это позволяет затем использовать стандартные механизмы отчетов 1С:

Результат = Новый ТаблицаЗначений;

Результат.Колонки.Добавить("Уровень");

Результат.Колонки.Добавить("Наименование");

Результат.Колонки.Добавить("Количество");

Процедура ЗаполнитьТаблицу(Узел, Уровень = 0)

НоваяСтрока = Результат.Добавить();

НоваяСтрока.Уровень = Уровень;

НоваяСтрока.Наименование = Узел.Наименование;

НоваяСтрока.Количество = Узел.Количество;

Если Узел.ЕстьСтроки() Тогда

Для Каждого ДочернийУзел Из Узел.Строки Цикл

ЗаполнитьТаблицу(ДочернийУзел, Уровень + 1);

КонецЦикла;

КонецЕсли;

КонецПроцедуры;

FAQ: Частые вопросы по обходу деревьев значений

Как обойти дерево значений в обратном порядке?

Для обратного обхода используйте цикл по индексам от последней строки к первой:

Для Инд = Дерево.Строки.Количество() - 1 По 0 Шаг -1 Цикл

Строка = Дерево.Строки.Получить(Инд);

// Обработка строки

КонецЦикла;

Для рекурсивного обратного обхода сначала обрабатывайте дочерние узлы, а затем текущий.

Можно ли обойти дерево значений параллельно в нескольких потоках?

Платформа 1С не поддерживает настоящую многопоточность в пользовательском коде. Однако для ускорения обработки больших деревьев можно:

  1. Разбить дерево на части (например, по корневым узлам)
  2. Обработать каждую часть в отдельном фоновом задании
  3. Объединить результаты

Пример кода с фоновыми заданиями доступен в документации по серверным вызовам.

Как найти конкретный узел в дереве по значению?

Для поиска узла используйте рекурсивную функцию с проверкой условия:

Функция НайтиУзелПоЗначению(Узел, ИскомоеЗначение, ИмяКолонки = "Наименование")

Если Узел[ИмяКолонки] = ИскомоеЗначение Тогда

Возврат Узел;

КонецЕсли;

Если Узел.ЕстьСтроки() Тогда

Для Каждого ДочернийУзел Из Узел.Строки Цикл

РезультатПоиска = НайтиУзелПоЗначению(ДочернийУзел, ИскомоеЗначение, ИмяКолонки);

Если РезультатПоиска <> Неопределено Тогда

Возврат РезультатПоиска;

КонецЕсли;

КонецЦикла;

КонецЕсли;

Возврат Неопределено;

КонецФункции;

Как экспортировать дерево значений в JSON?

Для экспорта в JSON используйте рекурсивную функцию построения структуры:

Функция ДеревоВJSON(Узел)

Результат = Новый Структура;

Для Каждого Колонка Из Узел.Колонки Цикл

Результат.Вставить(Колонка.Имя, Узел[Колонка.Имя]);

КонецЦикла;

Если Узел.ЕстьСтроки() Тогда

Дочерние = Новый Массив;

Для Каждого ДочернийУзел Из Узел.Строки Цикл

Дочерние.Добавить(ДеревоВJSON(ДочернийУзел));

КонецЦикла;

Результат.Вставить("Children", Дочерние);

КонецЕсли;

Возврат Результат;

КонецФункции;

Затем используйте ЗаписьJSON.Записать() для сохранения результата.

Как обработать только листья дерева (узлы без потомков)?

Для обработки только конечных узлов добавьте проверку Узел.ЕстьСтроки():

Процедура ОбработатьЛистья(Узел)

Если НЕ Узел.ЕстьСтроки() Тогда

// Обработка листа

Сообщить("Лист: " + Узел.Наименование);

Иначе

Для Каждого ДочернийУзел Из Узел.Строки Цикл

ОбработатьЛистья(ДочернийУзел);

КонецЦикла;

КонецЕсли;

КонецПроцедуры;