Разработка и Конфигурация · 11 мин
Как обойти дерево значений в 1С: способы и примеры кода
Как обойти дерево значений в 1С: рекурсивный обход, обход по уровням, поиск строк, работа с Родитель и Строки. Готовые примеры кода и типичные ошибки.
Дерево значений в 1С — это иерархическая структура, где у каждой строки есть коллекция подчинённых строк (Строки) и ссылка на родителя (Родитель). В отличие от таблицы значений, простым циклом Для Каждого по коллекции верхнего уровня обойти все данные нельзя — вы увидите только строки первого уровня. Поэтому обход дерева всегда строится либо на рекурсии, либо на явном управлении стеком строк.
Типовые задачи, где требуется обход дерева: заполнение дерева на форме, поиск строки по значению, подсчёт итогов по веткам, свёртка дерева в таблицу значений, проверка пометок в иерархическом списке. Ниже разобраны рабочие способы обхода с готовым кодом, их ограничения и частые ошибки.
Как устроено дерево значений и почему простой цикл не работает
Объект ДеревоЗначений имеет коллекцию Строки — это строки верхнего уровня. Каждая строка (объект СтрокаДереваЗначений) в свою очередь содержит собственную коллекцию Строки с вложенными элементами. Глубина вложенности не ограничена и заранее неизвестна.
Ключевые свойства строки дерева:
Строки— коллекция подчинённых строк;Родитель— ссылка на родительскую строку (у строк верхнего уровня —Неопределено);Уровень()— метод, возвращающий глубину строки (0 для верхнего уровня);- колонки, определённые в
Дерево.Колонки— обращение по имени, как в таблице значений.
Если написать Для Каждого Строка Из Дерево.Строки Цикл, цикл пройдёт только по строкам нулевого уровня. Все вложенные данные останутся необработанными — это самая частая ошибка начинающих разработчиков.
Рекурсивный обход дерева — универсальный способ
Рекурсия — стандартный приём: процедура обрабатывает коллекцию строк и для каждой строки вызывает саму себя, передавая коллекцию вложенных строк. Так обходятся все ветки независимо от глубины.
Процедура ОбойтиДеревоРекурсивно(СтрокиДерева, Уровень = 0)
Для Каждого Строка Из СтрокиДерева Цикл
// Обработка текущей строки
Сообщить(Строка(Уровень) + ": " + Строка.Наименование);
// Рекурсивный вызов для подчинённых строк
Если Строка.Строки.Количество() > 0 Тогда
ОбойтиДеревоРекурсивно(Строка.Строки, Уровень + 1);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// Вызов:
ОбойтиДеревоРекурсивно(Дерево.Строки);
Параметр Уровень не обязателен — глубину всегда можно получить через Строка.Уровень(), но передача параметром удобна, когда уровень нужен для отступов, накопления итогов или ограничения глубины обхода.
Проверка Строка.Строки.Количество() > 0 не обязательна — цикл по пустой коллекции просто не выполнится. Но явная проверка делает код читаемее и позволяет, например, отличать «папки» от «элементов».
Обход без рекурсии через стек
Если рекурсия нежелательна (очень глубокое дерево, требования стандарта, отладка), обход организуют через явный стек на массиве. Принцип: помещаете строки в массив, извлекаете по одной, обрабатываете и добавляете вложенные строки в конец массива.
Процедура ОбойтиДеревоБезРекурсии(Дерево)
Стек = Новый Массив;
Для Каждого Строка Из Дерево.Строки Цикл
Стек.Добавить(Строка);
КонецЦикла;
Пока Стек.Количество() > 0 Цикл
ТекущаяСтрока = Стек[Стек.Количество() - 1];
Стек.Удалить(Стек.Количество() - 1);
// Обработка строки
Сообщить(ТекущаяСтрока.Наименование);
Для Каждого Подчиненная Из ТекущаяСтрока.Строки Цикл
Стек.Добавить(Подчиненная);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Такой обход выполняется в глубину, но порядок обхода веток будет обратным. Если порядок важен, добавляйте подчинённые строки в стек в обратном порядке или используйте очередь (извлечение с индекса 0) для обхода по уровням.
Поиск строки в дереве значений
Метод НайтиПоЗначению
Коллекция строк дерева поддерживает метод НайтиПоЗначению(Значение, Колонка, ИскатьВПодчиненных). Третий параметр — ключевой: если передать Истина, поиск выполняется рекурсивно по всем вложенным строкам.
НайденнаяСтрока = Дерево.Строки.НайтиПоЗначению(
ИскомаяСсылка, "Номенклатура", Истина);
Если НайденнаяСтрока <> Неопределено Тогда
Сообщить("Найдено на уровне: " + НайденнаяСтрока.Уровень());
КонецЕсли;
Ограничение метода: ищется только точное совпадение по одной колонке. Для поиска по нескольким условиям или по частичному совпадению нужен собственный рекурсивный обход.
Поиск по произвольному условию
Функция НайтиСтрокуПоУсловию(СтрокиДерева, ИскомыйКод)
Для Каждого Строка Из СтрокиДерева Цикл
Если Строка.Код = ИскомыйКод Тогда
Возврат Строка;
КонецЕсли;
Найденная = НайтиСтрокуПоУсловию(Строка.Строки, ИскомыйКод);
Если Найденная <> Неопределено Тогда
Возврат Найденная;
КонецЕсли;
КонецЦикла;
Возврат Неопределено;
КонецФункции
Обратите внимание на приём «проброса» результата: если вложенный вызов вернул не Неопределено, значение сразу возвращается наверх, и обход прекращается. Без этого приёма поиск продолжится по всем веткам даже после нахождения строки.
- 1Определите задачу: полная обработка всех строк, поиск одной строки или сбор итогов
- 2Для полного обхода напишите рекурсивную процедуру с параметром-коллекцией строк
- 3Для поиска по одной колонке попробуйте НайтиПоЗначению с третьим параметром Истина
- 4Для сложного поиска напишите рекурсивную функцию с возвратом найденной строки
- 5Проверьте результат на дереве с вложенностью минимум 2–3 уровня и на пустом дереве
Подъём вверх по иерархии через Родитель
Иногда нужно не спуститься вниз, а подняться от найденной строки к корню — например, чтобы собрать полный путь строки или проверить признак у всех родителей. Для этого служит свойство Родитель.
Функция ПолучитьПутьСтроки(СтрокаДерева)
Путь = СтрокаДерева.Наименование;
ТекущийРодитель = СтрокаДерева.Родитель;
Пока ТекущийРодитель <> Неопределено Цикл
Путь = ТекущийРодитель.Наименование + " / " + Путь;
ТекущийРодитель = ТекущийРодитель.Родитель;
КонецЦикла;
Возврат Путь;
КонецФункции
У строк верхнего уровня Родитель равен Неопределено — это условие выхода из цикла. Учтите: если строка получена не из дерева, а создана отдельно, свойства Родитель и Уровень() могут быть недоступны или некорректны.
Обход дерева на форме: ДанныеФормыДерево
Когда дерево размещено на управляемой форме как реквизит типа «Дерево значений», в коде формы вы работаете не с ДеревоЗначений, а с объектом ДанныеФормыДерево. У него есть метод ПолучитьЭлементы() — аналог коллекции Строки.
Процедура ОбойтиДеревоФормы(ЭлементыДерева)
Для Каждого Элемент Из ЭлементыДерева Цикл
Элемент.Пометка = Истина; // пример обработки
ОбойтиДеревоФормы(Элемент.ПолучитьЭлементы());
КонецЦикла;
КонецПроцедуры
// Вызов из обработчика на форме:
ОбойтиДеревоФормы(ДеревоНоменклатуры.ПолучитьЭлементы());
Для поиска в ДанныеФормыДерево нет метода НайтиПоЗначению — используйте рекурсивный перебор. Зато у каждого элемента есть ПолучитьРодителя() и ПолучитьИдентификатор(), что позволяет связывать строки дерева с текущей строкой таблицы формы через Элементы.ИмяТаблицы.ТекущаяСтрока.
Если нужно перенести данные между ДеревоЗначений и реквизитом формы, используйте РеквизитФормыВЗначение("ИмяРеквизита") и ЗначениеВРеквизитФормы(Дерево, "ИмяРеквизита"). Помните: сериализация дерева между клиентом и сервером — дорогая операция, на больших деревьях минимизируйте число таких вызовов.
Подсчёт итогов по веткам дерева
Частая задача — посчитать сумму по колонке с учётом иерархии: итог родителя должен включать суммы всех подчинённых строк. Рекурсивная функция с возвратом суммы решает её компактно.
Функция РассчитатьИтоги(СтрокиДерева)
ИтогПоКоллекции = 0;
Для Каждого Строка Из СтрокиДерева Цикл
СуммаПодчиненных = РассчитатьИтоги(Строка.Строки);
Если СуммаПодчиненных <> 0 Тогда
Строка.Сумма = СуммаПодчиненных; // итог по ветке
КонецЕсли;
ИтогПоКоллекции = ИтогПоКоллекции + Строка.Сумма;
КонецЦикла;
Возврат ИтогПоКоллекции;
КонецФункции
Здесь обход идёт «снизу вверх»: сначала вычисляются итоги по вложенным веткам, потом результат поднимается к родителю. Если у строк-«листьев» сумма задана изначально, а у групп должна быть вычислена, логику присвоения можно разделить по признаку Строка.Строки.Количество() = 0.
Типичные ошибки при обходе дерева значений
Первая ошибка — перебор только верхнего уровня и уверенность, что обработано всё дерево. Проверяйте результат на тестовом дереве с вложенностью хотя бы в три уровня.
Вторая ошибка — потеря контекста при работе на форме: разработчик ищет метод НайтиПоЗначению у ДанныеФормыДерево, которого там нет, или пытается обратиться к Строки вместо ПолучитьЭлементы().
Третья ошибка — модификация коллекции во время обхода. Удаление строки внутри Для Каждого ломает итератор: часть строк будет пропущена без всякого сообщения об ошибке.
Проверка результата и FAQ
После реализации обхода проверьте три сценария: пустое дерево (код не должен падать), дерево из одной строки без вложенности, дерево с глубокой вложенностью и несколькими ветками. Для контроля полноты обхода удобно считать количество обработанных строк и сравнивать с ожидаемым.
Что важно запомнить
Полный обход дерева значений делается рекурсией по коллекциям Строки или явным стеком. Для поиска по одной колонке используйте НайтиПоЗначению с параметром Истина, для подъёма вверх — свойство Родитель. На управляемой форме работайте через ПолучитьЭлементы() и ПолучитьРодителя(). Не изменяйте коллекцию внутри цикла обхода.
Почему цикл «Для Каждого» по Дерево.Строки не обрабатывает вложенные строки?
Коллекция Строки содержит только строки одного уровня. Вложенные строки лежат в коллекции Строки каждой конкретной строки, поэтому без рекурсии или стека до них не добраться.
Как найти строку в дереве по значению колонки?
Используйте Дерево.Строки.НайтиПоЗначению(Значение, "ИмяКолонки", Истина). Третий параметр включает поиск во всех вложенных строках. Для поиска по нескольким условиям напишите рекурсивную функцию.
Чем отличается обход дерева на управляемой форме?
На форме реквизит дерева имеет тип ДанныеФормыДерево. Вместо Строки используется метод ПолучитьЭлементы(), вместо Родитель — ПолучитьРодителя(). Метода НайтиПоЗначению там нет, поиск делается рекурсивным перебором.
Опасна ли рекурсия при очень глубоком дереве?
При экстремально большой глубине вложенности возможно переполнение стека вызовов. На практике деревья редко имеют больше десятка уровней, но если глубина потенциально не ограничена, используйте нерекурсивный обход со стеком на массиве.
Как удалить из дерева строки по условию?
Нельзя удалять строки внутри цикла по той же коллекции. Сначала рекурсивно соберите строки-кандидаты в массив, затем после обхода удаляйте каждую через Родитель.Строки.Удалить(Строка) или из коллекции верхнего уровня.
Как преобразовать дерево значений в таблицу значений?
Прямого метода нет. Создайте таблицу значений с нужными колонками (при необходимости добавьте колонки «Уровень» и «Родитель») и заполните её рекурсивным обходом дерева, добавляя по одной строке таблицы на каждую строку дерева.