К статье
Практический материал

Разработка и Конфигурация · 12 мин

Как обойти строки дерева значений в 1С

Как программно обойти строки дерева значений в 1С: рекурсивный обход, поиск строк, обработка уровней. Примеры кода на встроенном языке, типичные ошибки и FAQ.

Дерево значений в 1С — это иерархическая структура данных, где каждая строка может содержать подчинённые строки. В отличие от таблицы значений, обойти её простым циклом «Для Каждого» недостаточно: такой цикл переберёт только строки верхнего уровня, а вложенные ветви останутся необработанными. Поэтому для полного обхода используют рекурсию — процедуру, которая вызывает сама себя для каждой подчинённой строки.

Задача обхода дерева возникает постоянно: при работе с планом счетов, иерархическими справочниками, деревом группировок СКД, движениями по регистрам с иерархией, при выводе отчётов и загрузке данных из XML. Ниже разобраны основные приёмы: рекурсивный обход, поиск строк по условию, обработка с учётом уровня вложенности и типичные ошибки, которые приводят к потере данных или зависанию кода.

Все примеры написаны на встроенном языке 1С и подходят как для тонкого, так и для толстого клиента. Код процедур обхода обычно размещают в общем модуле или модуле объекта, в зависимости от того, где используется дерево.

Что такое дерево значений и почему нужен рекурсивный обход

Дерево значений (объект ДеревоЗначений) состоит из строк типа СтрокаДереваЗначений. У каждой строки есть коллекция подчинённых строк, доступная через свойство Строки, и ссылка на родителя через свойство Родитель. У строк верхнего уровня свойство Родитель равно Неопределено.

Глубина вложенности заранее неизвестна: в одной ветви может быть два уровня, в другой — десять. Жёстко зашить в код вложенные циклы под конкретную глубину нельзя, поэтому единственный универсальный способ — рекурсия. Процедура получает коллекцию строк, перебирает их и для каждой строки вызывает себя же, передавая Строка.Строки.

Альтернатива рекурсии — итеративный обход с собственным стеком, но на практике в 1С он почти не применяется: рекурсивный вариант короче, читабельнее и надёжнее. Ограничение рекурсии одно — при экстремально глубокой вложенности (тысячи уровней) возможно переполнение стека вызовов, но для реальных учётных данных такая глубина практически не встречается.

Базовый рекурсивный обход дерева

Классическая схема выглядит так: одна процедура-«запускатель» передаёт в рекурсивную процедуру корневую коллекцию строк, а та обходит все уровни. Код размещается в модуле, где доступно дерево.

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

ОбойтиСтрокиРекурсивно(Дерево.Строки);

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

Процедура ОбойтиСтрокиРекурсивно(Строки)

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

// Здесь обработка текущей строки

Сообщить(Строка.Наименование);

// Рекурсивный вызов для подчинённых строк

ОбойтиСтрокиРекурсивно(Строка.Строки);

КонецЦикла;

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

Обратите внимание: рекурсивный вызов выполняется для каждой строки, даже если подчинённых строк нет. Пустая коллекция просто не войдёт в цикл, и вызов завершится мгновенно — дополнительная проверка Если Строка.Строки.Количество() > 0 не обязательна, хотя иногда её добавляют для читаемости.

Порядок обхода в этой схеме — «в глубину»: сначала обрабатывается строка, затем все её потомки, и только потом следующая строка того же уровня. Для большинства задач (подсчёт итогов, поиск, заполнение) именно такой порядок и нужен.

bsl
Процедура ОбойтиСтрокиРекурсивно(Строки)
  Для Каждого Строка Из Строки Цикл
    // обработка строки
    ОбойтиСтрокиРекурсивно(Строка.Строки);
  КонецЦикла;
КонецПроцедуры

Обход с учётом уровня вложенности

Часто нужно знать, на каком уровне находится строка: для отступов при выводе, для ограничения глубины обработки или для разной логики на разных уровнях. Уровень передаётся в рекурсивную процедуру дополнительным параметром, который увеличивается на единицу при каждом вызове.

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

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

Отступ = "";

Для Сч = 1 По Уровень Цикл

Отступ = Отступ + " ";

КонецЦикла;

Сообщить(Отступ + Строка.Наименование);

ОбойтиСтрокиРекурсивно(Строка.Строки, Уровень + 1);

КонецЦикла;

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

Если по условиям задачи обрабатывать нужно, например, только первые три уровня, добавьте в начало цикла проверку Если Уровень >= 3 Тогда Продолжить; — тогда более глубокие ветви будут пропущены, и рекурсия в них не уйдёт.

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

Поиск строк и сбор данных при обходе

Типовая задача — найти все строки, удовлетворяющие условию, независимо от уровня. Для этого в рекурсивную процедуру передают параметр-аккумулятор: массив, таблицу значений или структуру, куда складываются найденные строки.

Процедура НайтиСтрокиПоУсловию(Строки, Условие, Найденные)

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

Если Строка.Пометка = Условие Тогда

Найденные.Добавить(Строка);

КонецЕсли;

НайтиСтрокиПоУсловию(Строка.Строки, Условие, Найденные);

КонецЦикла;

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

// Вызов:

Найденные = Новый Массив;

НайтиСтрокиПоУсловию(Дерево.Строки, Истина, Найденные);

Если нужно найти только первую подходящую строку и остановиться, используйте функцию с возвратом результата: как только строка найдена, она возвращается вверх по цепочке вызовов, а дальнейший перебор прекращается. Это экономит время на больших деревьях.

Для поиска в пределах одного уровня без рекурсии у коллекции строк есть встроенные методы: Найти(Значение, ИмяКолонки) и НайтиСтроки(ПараметрыОтбора).

Обход дерева на форме и подсчёт итогов

Когда дерево значений является реквизитом формы, обращение к строкам идёт через Дерево.ПолучитьЭлементы() вместо Дерево.Строки — это особенность объекта ДанныеФормыДерево. Логика рекурсии при этом не меняется, меняется только способ получения коллекции подчинённых строк.

Процедура ОбойтиДеревоФормы(Элементы)

Для Каждого Строка Из Элементы Цикл

// обработка строки

ОбойтиДеревоФормы(Строка.ПолучитьЭлементы());

КонецЦикла;

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

// Вызов из обработчика на форме:

ОбойтиДеревоФормы(Дерево.ПолучитьЭлементы());

Частая прикладная задача — подсчёт итогов по ветвям: например, суммировать количество в строках и проставить итог в родительских строках. Здесь порядок обхода «в глубину» работает естественно: рекурсивная функция возвращает сумму подчинённых строк, а родительская строка прибавляет к ней собственное значение.

Функция ПосчитатьИтог(Строка)

Итог = Строка.Количество;

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

Итог = Итог + ПосчитатьИтог(Подстрока);

КонецЦикла;

Строка.ИтогоКоличество = Итог;

Возврат Итог;

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

Порядок действий для обхода дерева
  1. 1
    Определите, где лежит дерево: в модуле (Строки) или на форме (ПолучитьЭлементы())
  2. 2
    Напишите рекурсивную процедуру, принимающую коллекцию строк
  3. 3
    Внутри цикла Для Каждого разместите обработку строки и рекурсивный вызов
  4. 4
    При необходимости передайте дополнительные параметры: уровень, аккумулятор результатов
  5. 5
    Проверьте результат в отладчике: поставьте точку останова внутри цикла и убедитесь, что обрабатываются строки всех уровней

Типичные ошибки и ограничения

Самая распространённая ошибка — обход только верхнего уровня, когда разработчик пишет один цикл по Дерево.Строки и забывает про вложенность. Внешне код работает, ошибок не выдаёт, но часть данных молча теряется. Такие дефекты выявляются только при тестировании на данных с глубокой иерархией.

Вторая группа ошибок связана с модификацией коллекции во время обхода. Удалять или перемещать строки внутри цикла Для Каждого по той же коллекции нельзя — перебор собьётся или возникнет ошибка. Если нужно удалить часть строк, сначала соберите их в отдельный массив при обходе, а затем удаляйте в отдельном цикле.

Третья ошибка — путаница между Строки и ПолучитьЭлементы(). В модуле формы для реквизита типа «ДеревоЗначений» используется ПолучитьЭлементы(); обращение к Строки вызовет ошибку «Поле объекта не обнаружено». В серверных процедурах вне формы, наоборот, работает только Строки.

Типичные ошибки
01
Обход только верхнего уровня без рекурсии — вложенные строки молча пропускаются
02
Удаление строк внутри цикла обхода — перебор сбивается, часть строк пропускается или возникает ошибка
03
Использование Строки вместо ПолучитьЭлементы() на форме — ошибка «Поле объекта не обнаружено»
04
Рекурсивный вызов с передачей самой строки вместо её коллекции Строки — сигнатура процедуры ломается

Проверка результата и отладка

Надёжный способ убедиться, что обход полный, — посчитать строки двумя способами. Рекурсивный подсчёт: функция возвращает 1 + сумма подсчётов по подчинённым строкам для каждой строки. Контрольный способ — выгрузить дерево в таблицу значений или визуально сравнить количество обработанных строк с ожидаемым на тестовых данных.

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

Для больших деревьев полезно замерить время обхода. Рекурсивный обход сам по себе быстрый; если обход тормозит, причина почти всегда в том, что делается с каждой строкой: запросы к базе внутри цикла, обращения к СКД или запись данных. Выносите тяжёлые операции за пределы рекурсии — сначала соберите нужные данные в массив, потом обрабатывайте пакетно.

Контрольные точки

Как проверить результат

Количество обработанных строк
Норма

Совпадает с рекурсивным подсчётом всех строк дерева

Тревожный признак

Обработаны только строки верхнего уровня

Уровень строк в отладчике
Норма

Встречаются строки с уровнем больше 0

Тревожный признак

Все строки имеют уровень 0 — рекурсия не вызывается

Результат после удаления строк
Норма

Удалены все и только нужные строки

Тревожный признак

Часть строк пропущена или удалены лишние

FAQ

Можно ли обойти дерево значений без рекурсии?

Технически да — через собственный стек в виде массива или цикл с подъёмом по свойству Родитель. На практике такие варианты сложнее в написании и сопровождении, а выигрыша в скорости не дают. Рекурсия остаётся стандартным решением; стековый обход оправдан только при угрозе переполнения стека вызовов на экстремально глубоких деревьях.

Как обойти дерево в обратном порядке — сначала потомки, потом родители?

Перенесите обработку строки после рекурсивного вызова: сначала процедура обходит все подчинённые строки, затем обрабатывает текущую. Этот порядок называется обходом «снизу вверх» и нужен, например, когда итоги дочерних строк должны быть готовы до обработки родителя.

Как получить всех родителей текущей строки?

Используйте цикл по свойству Родитель: Пока ТекРодитель <> Неопределено Цикл.. ТекРодитель = ТекРодитель.Родитель;. На форме для ДанныеФормыДерево вместо свойства Родитель применяется метод ПолучитьРодителя() у элемента дерева.

Почему на форме обращение к Дерево.Строки выдаёт ошибку?

Реквизит формы типа «ДеревоЗначений» имеет тип ДанныеФормыДерево, у которого нет свойства Строки. Коллекцию элементов верхнего уровня возвращает метод ПолучитьЭлементы(), и тот же метод используется у каждой строки для получения подчинённых. Свойство Строки доступно только у объекта ДеревоЗначений вне контекста формы.

Как ограничить обход определённой глубиной?

Передайте в рекурсивную процедуру параметр уровня и добавьте проверку: если уровень достиг предела, не выполняйте рекурсивный вызов для подчинённых строк. Так обход остановится на нужной глубине, а лишние вызовы не будут тратить время.

Что делать, если при обходе нужно изменить значения строк?

Изменение значений колонок текущей строки внутри цикла безопасно — структура коллекции при этом не меняется. Опасны только операции, меняющие состав коллекции: добавление, удаление, перемещение строк. Их следует выполнять после завершения обхода по заранее собранному списку.

Итог

Что важно запомнить

Полный обход дерева значений делается рекурсивной процедурой: цикл по коллекции строк плюс вызов самой себя для подчинённых строк каждой строки. На форме вместо Строки используется ПолучитьЭлементы(). Удалять строки внутри обхода нельзя — сначала соберите их в массив. Полноту обхода проверяйте подсчётом строк и свойством Уровень() в отладчике.