Как перебрать все уровни ДеревоЗначений: рекурсивный и итеративный обход с готовым кодом
Содержание
Чтобы обойти все строки дерева значений в 1С, нужна рекурсия или собственный стек: цикл Для Каждого Строка Из Дерево.Строки перебирает только верхний уровень. У каждой строки есть своя коллекция Строки с подчинёнными, и спускаться в неё нужно явно.
Ниже код для платформы 8.3 на встроенном языке: рекурсивный обход, обход без рекурсии, работа с уровнями, путь к узлу, дерево на управляемой форме и удаление строк на ходу. Примеры не зависят от конфигурации — объект ДеревоЗначений одинаков в БП, ЗУП, УТ и в любой самописной базе.
Как устроено дерево и почему простой цикл видит только верх
Дерево значений — это колонки и вложенные коллекции строк. Схема такая:
- 🌳
Дерево.Строки— коллекция строк первого уровня (КоллекцияСтрокДереваЗначений); - 📄 каждая строка —
СтрокаДереваЗначенийсо значениями колонок; - 📂
Строка.Строки— коллекция её подчинённых строк, такая же по типу; - ⬆️
Строка.Родитель— родительская строка, у строк первого уровня —Неопределено.
Подготовим тестовое дерево, на нём будут все примеры:
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));
Офис = Дерево.Строки.Добавить();
Офис.Наименование = "Офис";
Бумага = Офис.Строки.Добавить();
Бумага.Наименование = "Бумага";
Бумага.Сумма = 1200;
Картриджи = Офис.Строки.Добавить();
Картриджи.Наименование = "Картриджи";
Картриджи.Сумма = 4500;
Склад = Дерево.Строки.Добавить();
Склад.Наименование = "Склад";
Простой цикл по Дерево.Строки выведет две строки: «Офис» и «Склад». «Бумага» и «Картриджи» лежат уровнем ниже, в Офис.Строки.
Для Каждого Строка Из Дерево.Строки Цикл
Сообщить(Строка.Наименование); // Офис, Склад
КонецЦикла;
Этого хватает, когда структура известна заранее и глубина фиксирована: например, два уровня «группа → элемент». Тогда вложенный цикл читается проще любой рекурсии:
Для Каждого Группа Из Дерево.Строки Цикл
Для Каждого Элемент Из Группа.Строки Цикл
Сообщить(Группа.Наименование + " / " + Элемент.Наименование);
КонецЦикла;
КонецЦикла;
Если глубина заранее неизвестна, вложенные циклы не годятся. Нужен один из способов ниже.
Рекурсивный обход дерева значений: универсальный вариант
Процедура получает коллекцию строк, обрабатывает каждую строку и вызывает сама себя для её подчинённых. Это обход в глубину: родитель, затем все его потомки, затем следующий родитель.
Процедура ОбойтиСтроки(КоллекцияСтрок, Уровень = 0)
Для Каждого Строка Из КоллекцияСтрок Цикл
Сообщить(СтрШаблон("%1 (уровень %2)", Строка.Наименование, Уровень));
ОбойтиСтроки(Строка.Строки, Уровень + 1);
КонецЦикла;
КонецПроцедуры
// Вызов
ОбойтиСтроки(Дерево.Строки);
Результат: «Офис (уровень 0)», «Бумага (уровень 1)», «Картриджи (уровень 1)», «Склад (уровень 0)».
Отдельно проверять, есть ли у строки подчинённые, не нужно. Для строки без потомков Строка.Строки — пустая коллекция, цикл внутри вызова просто не выполнится.
Обход снизу вверх: свернуть суммы в группы
Иногда родителя надо обработать после потомков. Типичный пример — посчитать сумму группы как сумму вложенных строк. Здесь рекурсивная функция сначала уходит вниз, а значение записывает на обратном пути:
Функция СвернутьСуммы(КоллекцияСтрок)
Итого = 0;
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Строка.Строки.Количество() > 0 Тогда
Строка.Сумма = СвернутьСуммы(Строка.Строки);
КонецЕсли;
Итого = Итого + Строка.Сумма;
КонецЦикла;
Возврат Итого;
КонецФункции
ВсегоПоДереву = СвернутьСуммы(Дерево.Строки);
На тестовом дереве «Офис» получит 1200 + 4500 = 5700, «Склад» останется с нулём, ВсегоПоДереву будет 5700.
Рекурсивные процедуры удобно держать в общем модуле с флажком «Сервер» или в модуле объекта. Если обход нужен в нескольких местах, передавайте в процедуру ещё и параметр-обработчик, например имя колонки, вместо копирования цикла.
Обход строк дерева без рекурсии: стек и очередь
Рекурсия ограничена глубиной стека вызовов. Для справочников и отчётов это не проблема: иерархия в десятки уровней встречается редко. Но если дерево строится из внешних данных с непредсказуемой глубиной, или вам нужен обход в ширину, используйте массив как стек или очередь.
В глубину через стек
Массив работает как стопка: кладём в конец, забираем с конца. Чтобы порядок совпал с рекурсивным, подчинённые кладём в обратном порядке — тогда первый потомок окажется сверху.
Стек = Новый Массив;
Верх = Дерево.Строки;
Всего = Верх.Количество();
Для Сч = 1 По Всего Цикл
Стек.Добавить(Верх[Всего - Сч]);
КонецЦикла;
Пока Стек.Количество() > 0 Цикл
Строка = Стек[Стек.ВГраница()];
Стек.Удалить(Стек.ВГраница());
Сообщить(Строка.Наименование);
Подчиненные = Строка.Строки;
Всего = Подчиненные.Количество();
Для Сч = 1 По Всего Цикл
Стек.Добавить(Подчиненные[Всего - Сч]);
КонецЦикла;
КонецЦикла;
Порядок вывода: «Офис», «Бумага», «Картриджи», «Склад» — тот же, что у рекурсии.
В ширину через очередь
Обход в ширину выдаёт сначала весь первый уровень, потом весь второй и так далее. Удаление из начала массива на каждом шаге медленное, поэтому вместо удаления двигаем указатель:
Очередь = Новый Массив;
Для Каждого Строка Из Дерево.Строки Цикл
Очередь.Добавить(Строка);
КонецЦикла;
Указатель = 0;
Пока Указатель < Очередь.Количество() Цикл
Строка = Очередь[Указатель];
Указатель = Указатель + 1;
Сообщить(Строка.Наименование);
Для Каждого Подчиненная Из Строка.Строки Цикл
Очередь.Добавить(Подчиненная);
КонецЦикла;
КонецЦикла;
Порядок: «Офис», «Склад», «Бумага», «Картриджи». Плата за простоту — в конце массив хранит ссылки на все строки дерева. На сотнях тысяч строк это заметно по памяти.
| Способ | Порядок строк | Когда брать | Ограничения |
|---|---|---|---|
| Вложенные циклы | По уровням вглубь | Глубина фиксирована, 2–3 уровня | Не работает при неизвестной глубине |
| Рекурсия | В глубину | Почти всегда, самый читаемый код | Глубина ограничена стеком вызовов |
| Стек на массиве | В глубину | Глубина непредсказуема | Код длиннее, легко перепутать порядок |
| Очередь на массиве | По уровням | Нужен весь уровень целиком | Держит в памяти ссылки на все строки |
Обход дерева значений с учётом уровней
Номер уровня строки возвращает метод Уровень(): у строк первого уровня он равен 0. Внутри рекурсии выгоднее передавать уровень параметром, как в примере выше: метод каждый раз поднимается к корню через родителей.
Типовые задачи с уровнями:
- 🔢 Ограничить глубину. Добавьте в рекурсию параметр
МаксУровеньи не спускайтесь дальше:
Процедура ОбойтиДоУровня(КоллекцияСтрок, МаксУровень, Уровень = 0)
Для Каждого Строка Из КоллекцияСтрок Цикл
Сообщить(Строка.Наименование);
Если Уровень < МаксУровень Тогда
ОбойтиДоУровня(Строка.Строки, МаксУровень, Уровень + 1);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
- 🍃 Только «листья». Строка без подчинённых определяется условием
Строка.Строки.Количество() = 0. Так из дерева группировок выбирают детальные записи. - 📁 Только группы. Обратное условие:
Строка.Строки.Количество() > 0. - 🧭 Строки конкретного уровня. В очереди из прошлого раздела храните пары «строка — уровень» в структуре и отбирайте нужный уровень.
Дерево из запроса с итогами
Самый частый источник дерева — запрос с секцией ИТОГИ ПО. Результат выгружают так:
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Каждая группировка из ИТОГИ ПО становится уровнем дерева, детальные записи — листьями. Глубина здесь известна заранее: она равна числу группировок плюс уровень детальных записей. Если иерархия справочника участвует в итогах (ИЕРАРХИЯ), глубина становится переменной — обходите рекурсией.
Путь к строке: подъём по родителям
Обход вниз — половина задач. Вторая половина — от найденной строки подняться вверх и собрать путь, например «Офис / Картриджи». Для этого идём по свойству Родитель, пока оно не станет Неопределено:
Функция ПутьКСтроке(Строка, Разделитель = " / ")
Части = Новый Массив;
Текущая = Строка;
Пока Текущая <> Неопределено Цикл
Части.Вставить(0, Текущая.Наименование);
Текущая = Текущая.Родитель;
КонецЦикла;
Возврат СтрСоединить(Части, Разделитель);
КонецФункции
Для строки «Картриджи» функция вернёт Офис / Картриджи. СтрСоединить есть в платформе с 8.3.6; на старых релизах соберите строку конкатенацией.
Найти строку по значению в любом месте дерева можно без ручного обхода. У коллекции строк есть метод НайтиСтроки с параметром поиска в подчинённых:
Отбор = Новый Структура("Наименование", "Картриджи");
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из Найденные Цикл
Сообщить(ПутьКСтроке(Строка));
КонецЦикла;
Второй параметр Истина включает поиск во всех вложенных уровнях. Без него метод смотрит только в той коллекции, у которой вызван. Для одного значения есть Найти(Значение, Колонки, Истина) — возвращает первую найденную строку или Неопределено.
Дерево на управляемой форме: ПолучитьЭлементы
На форме дерево хранится не как ДеревоЗначений, а как ДанныеФормыДерево. Коллекции свойства Строки у него нет, вместо неё вызывают метод ПолучитьЭлементы(). Он есть и у самого реквизита, и у каждого элемента. Вверх ведёт метод ПолучитьРодителя().
Рекурсия на клиенте выглядит так (реквизит формы называется Дерево):
&НаКлиенте
Процедура ОбойтиЭлементы(Элементы)
Для Каждого Элемент Из Элементы Цикл
Сообщить(Элемент.Наименование);
ОбойтиЭлементы(Элемент.ПолучитьЭлементы());
КонецЦикла;
КонецПроцедуры
// Вызов
ОбойтиЭлементы(Дерево.ПолучитьЭлементы());
Тот же обход годится, чтобы раскрыть все группы в табличном поле. Метод Развернуть элемента формы принимает идентификатор строки и флаг «со всеми подчинёнными»:
&НаКлиенте
Процедура РазвернутьВсе()
Для Каждого Элемент Из Дерево.ПолучитьЭлементы() Цикл
Элементы.Дерево.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
КонецЦикла;
КонецПроцедуры
Тяжёлую обработку (тысячи строк, обращения к базе) лучше делать на сервере с обычным деревом. Реквизит превращают в ДеревоЗначений и обратно:
&НаСервере
Процедура ПересчитатьНаСервере()
ДеревоОбъект = РеквизитФормыВЗначение("Дерево");
СвернутьСуммы(ДеревоОбъект.Строки);
ЗначениеВРеквизитФормы(ДеревоОбъект, "Дерево");
КонецПроцедуры
Объект ДеревоЗначений недоступен в тонком клиенте. Код с Новый ДеревоЗначений и обходом Строки должен выполняться в серверном контексте, а на клиенте работайте с ПолучитьЭлементы().
Как ускорить перебор строк дерева
На деревьях в несколько сотен строк разница между подходами незаметна. На десятках тысяч строк её видно по замеру производительности. Что даёт эффект:
- Не ищите строку обходом внутри другого обхода. Если для каждой строки нужно найти «пару» в том же дереве, один раз соберите
Соответствие«ключ → строка», потом берите строку по ключу. Индексов, как у таблицы значений, у дерева нет. - Используйте
НайтиСтроки(Отбор, Истина)вместо собственного поиска с условием в цикле. Метод работает внутри платформы и короче в коде. - Не вызывайте
Уровень()в горячем цикле. Передавайте уровень параметром рекурсии. - Не гоняйте дерево между клиентом и сервером. Каждый серверный вызов с реквизитом-деревом передаёт его данные. Соберите всю обработку в один серверный вызов.
- Не обращайтесь к базе на каждой строке. Чтение реквизита ссылки через точку внутри обхода — это запрос на строку. Получите нужные поля сразу в исходном запросе.
- Сортируйте один раз.
Дерево.Строки.Сортировать("Наименование", Истина)упорядочит все уровни сразу, если второй параметр —Истина.
Ошибки при обходе дерева значений
Удаление строк внутри Для Каждого. Если удалить текущую строку из коллекции, которую перебирает цикл, следующая строка сдвинется на её место и будет пропущена. Удаляйте обратным циклом по индексу, а потомков обрабатывайте раньше родителя:
Процедура УдалитьПустые(КоллекцияСтрок)
Всего = КоллекцияСтрок.Количество();
Для Сч = 1 По Всего Цикл
Строка = КоллекцияСтрок[Всего - Сч];
УдалитьПустые(Строка.Строки);
Если Строка.Сумма = 0 И Строка.Строки.Количество() = 0 Тогда
КоллекцияСтрок.Удалить(Строка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Граница Всего вычисляется один раз до цикла, а удаление строки с бо́льшим индексом не сдвигает строки с меньшими. На тестовом дереве процедура удалит «Склад»: сумма нулевая, подчинённых нет.
Обращение к Родитель.Наименование у строки первого уровня. Родителя у неё нет, свойство вернёт Неопределено, и обращение к полю вызовет ошибку. Проверяйте Строка.Родитель <> Неопределено.
Путаница Дерево.Строки и Строка.Строки. Внутри рекурсии нужно передавать коллекцию текущей строки. Если по ошибке передать Дерево.Строки, процедура будет снова и снова обходить верхний уровень, пока не исчерпает стек вызовов.
Добавление строк в коллекцию, которую обходите. Новая строка в конце коллекции попадёт в тот же цикл, и при определённой логике обход станет бесконечным. Новые строки собирайте в массив и добавляйте после цикла.
Обход Строки у данных формы. У ДанныеФормыДерево такого свойства нет — нужен ПолучитьЭлементы(). Обратная ошибка — вызов ПолучитьЭлементы() у серверного ДеревоЗначений.
Частые вопросы
Как узнать общее количество строк во всех уровнях дерева?
Количество() считает строки только одной коллекции. Для всего дерева пройдите его рекурсией или стеком и увеличивайте счётчик на каждой строке.Как скопировать дерево значений, чтобы обходить копию?
Скопировать() у дерева возвращает новое дерево с теми же колонками и строками. Изменения в копии не затрагивают исходник — удобно, если нужно удалять строки, не трогая оригинал.Можно ли превратить дерево в плоскую таблицу?
ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка). Уровень и путь к узлу запишите в дополнительные колонки.Почему после ЗначениеВРеквизитФормы свернулись все группы на форме?
Развернуть.Как прервать обход, когда нужная строка найдена?
Прервать. В рекурсии сделайте функцию, которая возвращает Истина при находке, и проверяйте результат после каждого вложенного вызова, чтобы выйти из всех уровней.