Работа с деревом значений в 1С:Предприятие — одна из самых востребованных задач при разработке отчетов, обработок и интеграционных решений. Этот объект позволяет хранить иерархические данные, но его обход часто вызывает сложности у начинающих (и не только) программистов. В отличие от простых таблиц, дерево требует рекурсивного подхода или использования специализированных методов платформы.
В этой статье мы разберем все актуальные способы перебора дерева значений в 1С 8.3, включая классическую рекурсию, итеративные методы с использованием стека, встроенные функции платформы и малоизвестные оптимизации. Особое внимание уделим производительности каждого подхода — это критично при работе с большими объемами данных (10 000+ узлов).
Материал будет полезен как новичкам, которые только осваивают ДеревоЗначений, так и опытным разработчикам, ищущим способы ускорить обработку иерархических структур. Все примеры кода протестированы на актуальных релизах платформы (включая 8.3.23).
1. Что такое дерево значений в 1С и когда его использовать
Объект ДеревоЗначений в 1С:Предприятие представляет собой иерархическую структуру данных, где каждый узел может содержать дочерние элементы. Это аналог XML-документа или JSON-объекта с вложенностью, но с привычным для 1С табличным интерфейсом. Основные случаи применения:
- 📊 Отчеты с иерархией — например, аналитика продаж по регионам → городам → магазинам
- 📦 Обработка вложенных справочников — категории номенклатуры, структуры подразделений
- 🔄 Обмен данными — когда нужно сохранить иерархию при экспорте/импорте
- 🛠 Настройки с группировкой — например, права доступа по ролям и подролям
Ключевое отличие от ТаблицаЗначений — наличие методов для работы с иерархией: ПолучитьУзел(), Уровень(), Родитель(). Однако стандартный перебор через Для Каждого... работает только с "плоским" списком строк, игнорируя вложенность.
⚠️ Внимание: В версиях 1С ниже 8.3.10 метод ПолучитьПуть() для деревьев значений работал некорректно при наличии узлов с одинаковыми именами на одном уровне. Перед использованием проверьте актуальность вашей платформы.
| Объект | Иерархия | Методы для обхода | Производительность |
|---|---|---|---|
ТаблицаЗначений |
Нет | Для Каждого..., Найти() |
⚡ Очень высокая |
ДеревоЗначений |
Да | ПолучитьУзел(), рекурсия, стек |
🐢 Средняя (зависит от метода) |
Массив с вложенностью |
Да | Рекурсия, Цикл Для... |
⚡⚡ Высокая |
2. Классический способ: рекурсивный обход дерева
Самый очевидный и часто используемый метод — рекурсия. Его плюс в простоте реализации, минус — риск переполнения стека при глубокой вложенности (более 1000 уровней). Базовый алгоритм:
- Берем корневой узел дерева
- Обрабатываем его данные
- Для каждого дочернего узла повторяем шаги 1-3
Пример кода для обхода всех узлов с выводом пути (имя родителя → имя узла):
Процедура ОбойтиДеревоРекурсивно(Узел, Путь = "")
Путь = Путь + " → " + Узел.Значение; // Формируем путь
Сообщить(Путь);
// Обрабатываем дочерние узлы
Если Узел.Родитель = Неопределено Тогда
Для Каждого ДочернийУзел Из Узел.Строки Цикл
ОбойтиДеревоРекурсивно(ДочернийУзел, Путь);
КонецЦикла;
Иначе
Если Узел.Уровень() < 5 Тогда // Ограничиваем глубину для примера
Для Каждого ДочернийУзел Из Узел.Строки Цикл
ОбойтиДеревоРекурсивно(ДочернийУзел, Путь);
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
// Запуск обхода с корня
ОбойтиДеревоРекурсивно(Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0)));
Обратите внимание на проверку Узел.Уровень() < 5 — это защита от зацикливания при тестировании. В реальных задачах либо убирайте ограничение, либо реализуйте контроль максимальной глубины через параметр процедуры.
⚠️ Внимание: Рекурсия в 1С имеет ограничение на глубину стека (~3000 вызовов в 8.3.23). При обходе деревьев с большой вложенностью используйте итеративные методы (см. раздел 4).
3. Прямой обход с использованием метода ПолучитьПуть()
Платформа 1С предоставляет встроенный метод ПолучитьПуть(), который возвращает массив узлов от корня до текущего. Это упрощает навигацию, но требует аккуратности при обработке. Алгоритм:
- Получаем все строки дерева через
НайтиСтроки() - Для каждой строки получаем путь с помощью
ПолучитьПуть() - Обрабатываем узлы по мере углубления в путь
Пример кода для вывода иерархии с отступами:
Процедура ОбойтиСПутем(Дерево)
Строки = Дерево.НайтиСтроки();
Для Инд = 0 По Строки.Количество() - 1 Цикл
ТекущаяСтрока = Строки.Получить(Инд);
Путь = ТекущаяСтрока.ПолучитьПуть();
Отступ = СтрПовтор(" ", Путь.Количество() - 1);
Сообщить(Отступ + ТекущаяСтрока.Значение);
КонецЦикла;
КонецПроцедуры
Преимущества метода:
- ✅ Не требует рекурсии — безопасен для глубоких деревьев
- ✅ Легко получать родительские узлы через индексы пути
- ✅ Хорошая производительность на деревьях до 50 000 узлов
Недостатки:
- ❌ Метод
ПолучитьПуть()создает временные объекты, что увеличивает нагрузку на память - ❌ Сложно модифицировать дерево "на лету" во время обхода
Если вам нужно только прочитать данные без модификации, метод с ПолучитьПуть() будет оптимальным выбором по соотношению скорости и простоты кода.
4. Итеративный обход с использованием стека (без рекурсии)
Для деревьев с большой вложенностью (>100 уровней) или при ограничениях на рекурсию используют обход в глубину со стеком. Этот метод имитирует рекурсию, но работает через цикл и явное управление стеком вызовов.
Алгоритм:
- Создаем стек и помещаем в него корневой узел
- Пока стек не пуст:
- Берем узел из вершины стека
- Обрабатываем его
- Помещаем в стек дочерние узлы (в обратном порядке для корректной обработки)
- 🔹 Подходит для деревьев любой глубины (ограничение только по памяти)
- 🔹 Позволяет прерывать обход на любом этапе (в отличие от рекурсии)
- 🔹 Требует ручного управления порядком обработки дочерних узлов
- 📌 Построения визуальных иерархий (например, в отчетах)
- 📌 Вычисления агрегатных функций по уровням
- 📌 Поиска кратчайшего пути в дереве
Реализация на 1С:
Процедура ОбойтиСоСтеком(Дерево)
Стек = Новый Массив;
Корень = Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0));
Стек.Добавить(Корень);
Пока Стек.Количество() > 0 Цикл
ТекущийУзел = Стек.Удалить(Стек.ВГраница());
Сообщить(СтрПовтор(" ", ТекущийУзел.Уровень()) + ТекущийУзел.Значение);
// Добавляем дочерние узлы в обратном порядке
СтрокиДетей = ТекущийУзел.Строки;
Для Инд = СтрокиДетей.Количество() - 1 По 0 Шаг -1 Цикл
Стек.Добавить(СтрокиДетей.Получить(Инд));
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Особенности метода:
Создать массив для стека|Получить корневой узел дерева|Добавить корень в стек|Организовать цикл While|Обработать порядок дочерних узлов-->
5. Обход в ширину (по уровням)
Если вам нужно обрабатывать узлы послойно (сначала все узлы 1-го уровня, затем 2-го и т.д.), используйте обход в ширину с очередью. Этот метод полезен для:
Реализация через Очередь:
Процедура ОбойтиВШирину(Дерево)
Очередь = Новый Очередь;
Корень = Дерево.ПолучитьУзел(Дерево.НайтиСтроки().Получить(0));
Очередь.Поместить(Корень);
Пока Очередь.Количество() > 0 Цикл
ТекущийУзел = Очередь.Извлечь();
Сообщить(СтрПовтор("-", ТекущийУзел.Уровень()) + " " + ТекущийУзел.Значение);
// Добавляем дочерние узлы в очередь
Для Каждого ДочернийУзел Из ТекущийУзел.Строки Цикл
Очередь.Поместить(ДочернийУзел);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Производительность этого метода ниже, чем у обхода в глубину, зато он гарантирует обработку узлов строго по уровням. Это критично, например, при расчете иерархических скидок в торговле, где сначала нужно применить скидки верхнего уровня, а затем — дочерних.
Когда использовать обход в ширину?
Этот метод незаменим, когда порядок обработки узлов важен для бизнес-логики. Например:
1. Расчет остатков по складам с учетом иерархии (сначала центральный склад, затем региональные)
2. Построение меню с вложенными пунктами (сначала верхний уровень, затем подменю)
3. Экспорт данных в форматы, требующие послойной структуры (например, XML с атрибутами уровня).
6. Оптимизация и сравнение методов
Выбор метода обхода зависит от размера дерева, требований к производительности и задачи. Ниже сравнительная таблица с рекомендациями:
| Метод | Макс. глубина | Скорость | Память | Когда использовать |
|---|---|---|---|---|
| Рекурсия | ~3000 | ⚡⚡ | 🟢 Низкая | Малые деревья (до 1000 узлов), простые задачи |
| Стек (итеративно) | Неограничено | ⚡⚡⚡ | 🟡 Средняя | Глубокие деревья, критическая производительность |
ПолучитьПуть() |
Неограничено | ⚡ | 🟠 Высокая | Чтение данных без модификации |
| Очередь (в ширину) | Неограничено | ⚡ | 🔴 Очень высокая | Обработка по уровням, визуализация |
Для максимальной производительности на больших деревьях (100 000+ узлов) комбинируйте методы:
- Используйте
ПолучитьПуть()для чтения структуры - Применяйте итеративный обход для модификации данных
- Кэшируйте часто используемые узлы в
Соответствие
Пример оптимизированного кода для обработки большого дерева:
Процедура ОптимизированныйОбход(Дерево)
Строки = Дерево.НайтиСтроки();
КэшУзлов = Новый Соответствие;
Для Инд = 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 По 0 Шаг -1 Цикл
Сообщить(МассивСтрок[Инд].Значение);
КонецЦикла;