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

В этой статье мы разберем все актуальные способы перебора дерева значений в 1С 8.3, включая классическую рекурсию, итеративные методы с использованием стека, встроенные функции платформы и малоизвестные оптимизации. Особое внимание уделим производительности каждого подхода — это критично при работе с большими объемами данных (10 000+ узлов).

Материал будет полезен как новичкам, которые только осваивают ДеревоЗначений, так и опытным разработчикам, ищущим способы ускорить обработку иерархических структур. Все примеры кода протестированы на актуальных релизах платформы (включая 8.3.23).

1. Что такое дерево значений в 1С и когда его использовать

Объект ДеревоЗначений в 1С:Предприятие представляет собой иерархическую структуру данных, где каждый узел может содержать дочерние элементы. Это аналог XML-документа или JSON-объекта с вложенностью, но с привычным для 1С табличным интерфейсом. Основные случаи применения:

  • 📊 Отчеты с иерархией — например, аналитика продаж по регионам → городам → магазинам
  • 📦 Обработка вложенных справочников — категории номенклатуры, структуры подразделений
  • 🔄 Обмен данными — когда нужно сохранить иерархию при экспорте/импорте
  • 🛠 Настройки с группировкой — например, права доступа по ролям и подролям

Ключевое отличие от ТаблицаЗначений — наличие методов для работы с иерархией: ПолучитьУзел(), Уровень(), Родитель(). Однако стандартный перебор через Для Каждого... работает только с "плоским" списком строк, игнорируя вложенность.

⚠️ Внимание: В версиях 1С ниже 8.3.10 метод ПолучитьПуть() для деревьев значений работал некорректно при наличии узлов с одинаковыми именами на одном уровне. Перед использованием проверьте актуальность вашей платформы.
Объект Иерархия Методы для обхода Производительность
ТаблицаЗначений Нет Для Каждого..., Найти() ⚡ Очень высокая
ДеревоЗначений Да ПолучитьУзел(), рекурсия, стек 🐢 Средняя (зависит от метода)
Массив с вложенностью Да Рекурсия, Цикл Для... ⚡⚡ Высокая

2. Классический способ: рекурсивный обход дерева

Самый очевидный и часто используемый метод — рекурсия. Его плюс в простоте реализации, минус — риск переполнения стека при глубокой вложенности (более 1000 уровней). Базовый алгоритм:

  1. Берем корневой узел дерева
  2. Обрабатываем его данные
  3. Для каждого дочернего узла повторяем шаги 1-3

Пример кода для обхода всех узлов с выводом пути (имя родителя → имя узла):

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

Путь = Путь + " → " + Узел.Значение; // Формируем путь

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

// Обрабатываем дочерние узлы

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

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

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

КонецЦикла;

Иначе

Если Узел.Уровень() < 5 Тогда // Ограничиваем глубину для примера

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

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

КонецЦикла;

КонецЕсли;

КонецЕсли;

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

// Запуск обхода с корня

ОбойтиДеревоРекурсивно(Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0)));

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

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

3. Прямой обход с использованием метода ПолучитьПуть()

Платформа 1С предоставляет встроенный метод ПолучитьПуть(), который возвращает массив узлов от корня до текущего. Это упрощает навигацию, но требует аккуратности при обработке. Алгоритм:

  1. Получаем все строки дерева через НайтиСтроки()
  2. Для каждой строки получаем путь с помощью ПолучитьПуть()
  3. Обрабатываем узлы по мере углубления в путь

Пример кода для вывода иерархии с отступами:

Процедура ОбойтиСПутем(Дерево)

Строки = Дерево.НайтиСтроки();

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

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

Путь = ТекущаяСтрока.ПолучитьПуть();

Отступ = СтрПовтор(" ", Путь.Количество() - 1);

Сообщить(Отступ + ТекущаяСтрока.Значение);

КонецЦикла;

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

Преимущества метода:

  • ✅ Не требует рекурсии — безопасен для глубоких деревьев
  • ✅ Легко получать родительские узлы через индексы пути
  • ✅ Хорошая производительность на деревьях до 50 000 узлов

Недостатки:

  • ❌ Метод ПолучитьПуть() создает временные объекты, что увеличивает нагрузку на память
  • ❌ Сложно модифицировать дерево "на лету" во время обхода
💡

Если вам нужно только прочитать данные без модификации, метод с ПолучитьПуть() будет оптимальным выбором по соотношению скорости и простоты кода.

4. Итеративный обход с использованием стека (без рекурсии)

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

Алгоритм:

  1. Создаем стек и помещаем в него корневой узел
  2. Пока стек не пуст:
    • Берем узел из вершины стека
    • Обрабатываем его
    • Помещаем в стек дочерние узлы (в обратном порядке для корректной обработки)
  3. Реализация на 1С:

    Процедура ОбойтиСоСтеком(Дерево)
    

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

    Корень = Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0));

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

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

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

    Сообщить(СтрПовтор(" ", ТекущийУзел.Уровень()) + ТекущийУзел.Значение);

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

    СтрокиДетей = ТекущийУзел.Строки;

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

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

    КонецЦикла;

    КонецЦикла;

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

    Особенности метода:

    • 🔹 Подходит для деревьев любой глубины (ограничение только по памяти)
    • 🔹 Позволяет прерывать обход на любом этапе (в отличие от рекурсии)
    • 🔹 Требует ручного управления порядком обработки дочерних узлов

    Создать массив для стека|Получить корневой узел дерева|Добавить корень в стек|Организовать цикл While|Обработать порядок дочерних узлов-->

    5. Обход в ширину (по уровням)

    Если вам нужно обрабатывать узлы послойно (сначала все узлы 1-го уровня, затем 2-го и т.д.), используйте обход в ширину с очередью. Этот метод полезен для:

    • 📌 Построения визуальных иерархий (например, в отчетах)
    • 📌 Вычисления агрегатных функций по уровням
    • 📌 Поиска кратчайшего пути в дереве

Реализация через Очередь:

Процедура ОбойтиВШирину(Дерево)

Очередь = Новый Очередь;

Корень = Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0));

Очередь.Поместить(Корень);

Пока Очередь.Количество() > 0 Цикл

ТекущийУзел = Очередь.Извлечь();

Сообщить(СтрПовтор("-", ТекущийУзел.Уровень()) + " " + ТекущийУзел.Значение);

// Добавляем дочерние узлы в очередь

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

Очередь.Поместить(ДочернийУзел);

КонецЦикла;

КонецЦикла;

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

Производительность этого метода ниже, чем у обхода в глубину, зато он гарантирует обработку узлов строго по уровням. Это критично, например, при расчете иерархических скидок в торговле, где сначала нужно применить скидки верхнего уровня, а затем — дочерних.

Когда использовать обход в ширину?

Этот метод незаменим, когда порядок обработки узлов важен для бизнес-логики. Например:

1. Расчет остатков по складам с учетом иерархии (сначала центральный склад, затем региональные)

2. Построение меню с вложенными пунктами (сначала верхний уровень, затем подменю)

3. Экспорт данных в форматы, требующие послойной структуры (например, XML с атрибутами уровня).

6. Оптимизация и сравнение методов

Выбор метода обхода зависит от размера дерева, требований к производительности и задачи. Ниже сравнительная таблица с рекомендациями:

Метод Макс. глубина Скорость Память Когда использовать
Рекурсия ~3000 ⚡⚡ 🟢 Низкая Малые деревья (до 1000 узлов), простые задачи
Стек (итеративно) Неограничено ⚡⚡⚡ 🟡 Средняя Глубокие деревья, критическая производительность
ПолучитьПуть() Неограничено 🟠 Высокая Чтение данных без модификации
Очередь (в ширину) Неограничено 🔴 Очень высокая Обработка по уровням, визуализация

Для максимальной производительности на больших деревьях (100 000+ узлов) комбинируйте методы:

  1. Используйте ПолучитьПуть() для чтения структуры
  2. Применяйте итеративный обход для модификации данных
  3. Кэшируйте часто используемые узлы в Соответствие

Пример оптимизированного кода для обработки большого дерева:

Процедура ОптимизированныйОбход(Дерево)

Строки = Дерево.НайтиСтроки();

КэшУзлов = Новый Соответствие;

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

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

Если НЕ КэшУзлов.Содержит(ТекущаяСтрока.Ссылка) Тогда

КэшУзлов.Вставить(ТекущаяСтрока.Ссылка, ТекущаяСтрока);

Путь = ТекущаяСтрока.ПолучитьПуть();

// Обработка...

КонецЕсли;

КонецЦикла;

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

💡

Для деревьев более 50 000 узлов всегда используйте итеративные методы (стек или очередь) и кэширование. Рекурсия в таких случаях приведет к переполнению стека или критическому падению производительности.

7. Практические примеры применения

Рассмотрим реальные сценарии, где обход дерева значений решает бизнес-задачи:

Пример 1: Расчет иерархических скидок

Задача: Применить скидки по категориям номенклатуры, где дочерние категории наследуют скидки родительских.

Процедура РассчитатьСкидки(ДеревоКатегорий, ТаблицаНоменклатуры)

Для Каждого СтрокаКатегории Из ДеревоКатегорий.НайтиСтроки() Цикл

Путь = СтрокаКатегории.ПолучитьПуть();

МаксимальнаяСкидка = 0;

// Ищем максимальную скидку по всему пути

Для Каждого УзелИзПути Из Путь Цикл

Если УзелИзПути.Скидка > МаксимальнаяСкидка Тогда

МаксимальнаяСкидка = УзелИзПути.Скидка;

КонецЕсли;

КонецЦикла;

// Применяем скидку ко всей номенклатуре категории

Для Каждого СтрокаНоменклатуры Из ТаблицаНоменклатуры Цикл

Если СтрокаНоменклатуры.Категория = СтрокаКатегории.Ссылка Тогда

СтрокаНоменклатуры.Цена = СтрокаНоменклатуры.Цена * (1 - МаксимальнаяСкидка/100);

КонецЕсли;

КонецЦикла;

КонецЦикла;

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

Пример 2: Построение отчета по структуре подразделений

Задача: Сформировать отчет с вложенностью подразделений и количеством сотрудников в каждом.

Процедура СформироватьОтчетПоПодразделениям(ДеревоПодразделений)

Результат = Новый ДеревоЗначений;

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

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

Процедура ОбойтиПодразделение(Узел, РодительскаяСтрока = Неопределено)

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

ТекущаяСтрока.Подразделение = Узел.Значение;

ТекущаяСтрока.Сотрудников = Узел.КоличествоСотрудников;

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

ТекущаяСтрока.Родитель = РодительскаяСтрока;

КонецЕсли;

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

ОбойтиПодразделение(ДочернееПодразделение, ТекущаяСтрока);

КонецЦикла;

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

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

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

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

Пример 3: Экспорт иерархии в JSON

Задача: Преобразовать дерево значений в JSON-структуру для передачи во внешнюю систему.

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

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

Результат.Вставить("name", Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0)).Значение);

Результат.Вставить("children", Новый Массив);

Процедура ДобавитьУзлы(Узел, РодительскийМассив)

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

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

НовыйУзел.Вставить("name", ДочернийУзел.Значение);

НовыйУзел.Вставить("children", Новый Массив);

РодительскийМассив.Добавить(НовыйУзел);

ДобавитьУзлы(ДочернийУзел, НовыйУзел.children);

КонецЦикла;

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

ДобавитьУзлы(Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0)), Результат.children);

Возврат JSON.Записать(Результат);

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

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

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

Используйте следующий код:

Функция МаксимальныйУровень(Дерево)

МаксУровень = 0;

Для Каждого Строка Из Дерево.НайтиСтроки() Цикл

Если Строка.Уровень() > МаксУровень Тогда

МаксУровень = Строка.Уровень();

КонецЕсли;

КонецЦикла;

Возврат МаксУровень;

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

Для больших деревьев (>10 000 узлов) этот метод может работать медленно. В таких случаях ведите учет уровня при обходе.

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

Да, но потребуется добавить колонку для хранения информации о родительских узлах. Пример:

Процедура ДеревоВТаблицу(Дерево, Таблица)

Для Каждого СтрокаДерева Из Дерево.НайтиСтроки() Цикл

НоваяСтрока = Таблица.Строки.Добавить();

ЗаполнитьЗначенияСтроки(НоваяСтрока, СтрокаДерева);

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

Если Путь.Количество() > 1 Тогда

НоваяСтрока.Родитель = Путь.Получить(Путь.Количество() - 2).Значение;

КонецЕсли;

КонецЦикла;

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

Обратите внимание, что в таблице значений не сохраняется вложенность — только ссылки на родителей.

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

Используйте метод НайтиСтроки() с параметром:

НайденныеСтроки = Дерево.НайтиСтроки(Новый Структура("Значение", ИскомоеЗначение));

Если НайденныеСтроки.Количество() > 0 Тогда

Узел = Дерево.ПолучитьУзел(НайденныеСтроки.Получить(0));

КонецЕсли;

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

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

Это типичная ошибка, когда:

  • Вы используете Для Каждого по Дерево.Строки вместо НайтиСтроки()
  • В дереве есть циклические ссылки (узел ссылается сам на себя)
  • Вы модифицируете дерево во время обхода

Решение: всегда используйте НайтиСтроки() для получения актуального списка строк.

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

Для этого:

  1. Соберите все узлы в массив через НайтиСтроки()
  2. Отсортируйте массив по убыванию уровня
  3. Обрабатывайте узлы в обратном порядке

Пример кода:

Строки = Дерево.НайтиСтроки();

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

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

МассивСтрок.Добавить(Строка);

КонецЦикла;

МассивСтрок.СортироватьПоУбыванию("Уровень");

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

Сообщить(МассивСтрок[Инд].Значение);

КонецЦикла;