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