Обход строк ДеревоЗначений на встроенном языке: рекурсия, стек, очередь и поиск

· 10 мин

Содержание

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

Способов обхода пять: рекурсия, цикл со стеком, обход по уровням через очередь, поиск методами Найти и НайтиСтроки с флагом «включать подчинённые» и подъём от строки к корню через свойство Родитель. Все примеры ниже — для 1С 8.3, работают в любой конфигурации, потому что дерево значений — объект платформы, а не БП или УТ.

Как устроено дерево значений и откуда оно берётся

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

Что нужно знать для обхода:

  • 🌳 Дерево.Строки — коллекция строк только верхнего уровня (тип КоллекцияСтрокДереваЗначений).
  • 📂 Строка.Строки — подчинённые строки конкретной строки, та же коллекция.
  • ⬆️ Строка.Родитель — родительская строка; у строк верхнего уровня это Неопределено.
  • 🔢 Строка.Уровень() — глубина: 0 для верхнего уровня, 1 для его детей и так далее.
  • 🔍 Найти() и НайтиСтроки() у коллекции строк умеют искать с учётом вложенных строк.

Откуда дерево обычно появляется в коде:

ИсточникКак получить
Запрос с ИТОГИ ПОРезультат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам)
Иерархический справочникРезультат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией) с итогами ИЕРАРХИЯ
Реквизит формы типа «Дерево значений»РеквизитФормыВЗначение("ИмяРеквизита") на сервере
Построение вручнуюНовый ДеревоЗначений, затем Колонки.Добавить() и Строки.Добавить()

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

Для примеров возьмём дерево с колонками Наименование (строка) и Сумма (число):

Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));

Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Канцтовары";

Товар = Группа.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Сумма = 1500;

Товар = Группа.Строки.Добавить();
Товар.Наименование = "Ручки";
Товар.Сумма = 300;

Рекурсивный перебор дерева значений

Рекурсия — самый короткий и читаемый способ. Процедура обрабатывает строки одной коллекции и для каждой строки вызывает саму себя с её подчинёнными строками.

Процедура ОбойтиСтроки(КоллекцияСтрок)

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

		// Обработка строки
		Сообщить(СтрШаблон("Уровень %1: %2", Строка.Уровень(), Строка.Наименование));

		// Спуск к подчинённым строкам
		Если Строка.Строки.Количество() > 0 Тогда
			ОбойтиСтроки(Строка.Строки);
		КонецЕсли;

	КонецЦикла;

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

// Вызов
ОбойтиСтроки(Дерево.Строки);

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

Проверка Количество() > 0 не обязательна: цикл по пустой коллекции просто не выполнится. Но она экономит лишний вызов процедуры на каждой конечной строке и делает код понятнее.

💡 Если нужно обрабатывать родителя после детей (например, суммировать итоги снизу вверх), перенесите строку обработки ниже рекурсивного вызова.

Рекурсивная функция: итоги групп снизу вверх

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

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

	Если Строка.Строки.Количество() = 0 Тогда
		Возврат Строка.Сумма;
	КонецЕсли;

	Итого = 0;
	Для Каждого Подчиненная Из Строка.Строки Цикл
		Итого = Итого + ПосчитатьИтог(Подчиненная);
	КонецЦикла;

	Строка.Сумма = Итого;
	Возврат Итого;

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

Для Каждого Строка Из Дерево.Строки Цикл
	ПосчитатьИтог(Строка);
КонецЦикла;

На нашем примере группа «Канцтовары» получит сумму 1500 + 300 = 1800.

Рекурсивную процедуру размещайте в общем модуле или модуле объекта, где она доступна в нужном контексте. Если вызываете её на сервере из формы, процедура тоже должна быть серверной (&НаСервере или &НаСервереБезКонтекста), иначе дерево значений на клиент не попадёт — этот тип на клиенте недоступен.

Путь к строке и поиск без ручного цикла

Иногда обходить всё дерево не нужно: требуется найти конкретные строки или узнать, где строка лежит в иерархии. Для этого есть готовые методы.

Поиск по всем уровням: Найти и НайтиСтроки

У коллекции строк третий параметр метода Найти и второй параметр НайтиСтроки включают поиск в подчинённых строках:

// Первая строка с таким значением в колонке «Наименование» на любом уровне
Строка = Дерево.Строки.Найти("Ручки", "Наименование", Истина);
Если Строка <> Неопределено Тогда
	Строка.Сумма = 350;
КонецЕсли;

// Все строки с нулевой суммой на любом уровне
Отбор = Новый Структура("Сумма", 0);
НайденныеСтроки = Дерево.Строки.НайтиСтроки(Отбор, Истина);

Для Каждого Строка Из НайденныеСтроки Цикл
	Сообщить(Строка.Наименование);
КонецЦикла;

НайтиСтроки возвращает обычный массив. Его удобно перебирать одним плоским циклом — это фактически «прямой обход» нужной части дерева без рекурсии в вашем коде.

💡 Если нужно пройти все строки плоским списком, добавьте служебную колонку-флаг с одинаковым значением во всех строках (например, при построении дерева) и найдите строки по ней через НайтиСтроки(..., Истина). Порядок строк в массиве при этом не гарантируйте в логике: если он важен, используйте рекурсию.

Путь строки через свойство Родитель

Отдельного метода, возвращающего путь строки, у дерева значений нет. Путь собирают подъёмом по свойству Родитель до верхнего уровня:

Функция ПутьКСтроке(Строка)

	Части = Новый Массив;
	Текущая = Строка;

	Пока Текущая <> Неопределено Цикл
		Части.Вставить(0, Текущая.Наименование);
		Текущая = Текущая.Родитель;
	КонецЦикла;

	Возврат СтрСоединить(Части, " / ");

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

Для строки «Ручки» функция вернёт Канцтовары / Ручки. Такой путь пригождается в сообщениях об ошибках: пользователь сразу видит, в какой группе проблема.

Перебор дерева значений без рекурсии: цикл со стеком

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

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

Стек = Новый Массив;

// Кладём в обратном порядке, чтобы первой снялась первая строка
Строки = Дерево.Строки;
Для Сч = 1 По Строки.Количество() Цикл
	Стек.Добавить(Строки[Строки.Количество() - Сч]);
КонецЦикла;

Пока Стек.Количество() > 0 Цикл

	Индекс = Стек.ВГраница();
	Строка = Стек[Индекс];
	Стек.Удалить(Индекс);

	// Обработка строки
	Сообщить(СтрШаблон("Уровень %1: %2", Строка.Уровень(), Строка.Наименование));

	// Дети — тоже в обратном порядке
	Подчиненные = Строка.Строки;
	Для Сч = 1 По Подчиненные.Количество() Цикл
		Стек.Добавить(Подчиненные[Подчиненные.Количество() - Сч]);
	КонецЦикла;

КонецЦикла;

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

Снимаем элемент с конца массива (ВГраница()), а не с начала: удаление первого элемента заставляет массив сдвигать все остальные, и на больших деревьях это заметно.

Обход дерева по уровням (в ширину)

Обход в ширину сначала проходит все строки уровня 0, потом все строки уровня 1 и так далее. Нужен, когда важен именно уровень: проверить группы раньше товаров, вывести структуру подразделений «этажами», остановиться на заданной глубине.

Вместо стека — очередь. Удалять элементы из начала массива не будем: двигаем указатель позиции.

Очередь = Новый Массив;
Для Каждого Строка Из Дерево.Строки Цикл
	Очередь.Добавить(Строка);
КонецЦикла;

Позиция = 0;
МаксимальныйУровень = 1; // глубже не спускаемся

Пока Позиция < Очередь.Количество() Цикл

	Строка = Очередь[Позиция];
	Позиция = Позиция + 1;

	Сообщить(СтрШаблон("Уровень %1: %2", Строка.Уровень(), Строка.Наименование));

	Если Строка.Уровень() < МаксимальныйУровень Тогда
		Для Каждого Подчиненная Из Строка.Строки Цикл
			Очередь.Добавить(Подчиненная);
		КонецЦикла;
	КонецЕсли;

КонецЦикла;

Ограничение глубины здесь задаётся одной строкой: при МаксимальныйУровень = 1 обработаются группы и их прямые дети, внуки — нет. В рекурсивном варианте для этого пришлось бы передавать текущую глубину параметром.

Минус: к концу обхода массив содержит ссылки на все пройденные строки. Для деревьев в десятки тысяч строк это лишняя память, но обычно некритично.

Какой способ перебора выбрать: сравнение

СпособКогда подходитПорядокОграничения
РекурсияБольшинство задач: печать, итоги, проверкиСверху вниз по веткамОчень глубокая вложенность упирается в стек вызовов
Стек на массивеНужен один цикл, удобная отладка, глубокое деревоКак у рекурсииКод длиннее, легко перепутать порядок добавления
Очередь по уровнямВажен уровень, нужно ограничить глубинуУровень за уровнемМассив растёт до размера дерева
НайтиСтроки(..., Истина)Нужны строки по условию на равенствоНе закладывайтесьТолько отбор по равенству значений колонок
Подъём по РодительПуть строки, проверка «входит ли в группу»Снизу вверхОбходит одну ветку, не всё дерево

По скорости все способы, которые проходят каждую строку один раз, сопоставимы. Заметно ускоряют обход другие вещи:

  • ⚡ Не вызывайте внутри цикла запросы к базе и обращения через точку к ссылкам (Строка.Номенклатура.Артикул). Всё нужное заранее получите запросом в колонки дерева.
  • ⚡ Для поиска по условию используйте НайтиСтроки — платформа ищет быстрее, чем цикл на встроенном языке.
  • ⚡ Не перебирайте дерево на клиенте ради расчётов: передайте задачу на сервер одним вызовом.
  • ⚡ Если вложенности нет (один уровень), это не дерево, а таблица — используйте таблицу значений.
Что нужно сделать с деревом?

Обход дерева значений на управляемой форме

На форме реквизит типа «Дерево значений» превращается в ДанныеФормыДерево. У него другие методы, и код с .Строки там не работает:

Объект на сервереОбъект на форме
Дерево.СтрокиДеревоРеквизит.ПолучитьЭлементы()
Строка.СтрокиЭлемент.ПолучитьЭлементы()
Строка.РодительЭлемент.ПолучитьРодителя()

Рекурсивный обход прямо по данным формы (работает и на клиенте):

&НаКлиенте
Процедура ОбойтиЭлементы(Элементы)

	Для Каждого Элемент Из Элементы Цикл
		Элемент.Сумма = Окр(Элемент.Сумма, 0);
		ОбойтиЭлементы(Элемент.ПолучитьЭлементы());
	КонецЦикла;

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

// Вызов
ОбойтиЭлементы(ДеревоТоваров.ПолучитьЭлементы());

Если нужны методы настоящего дерева (НайтиСтроки, Колонки), преобразуйте реквизит на сервере и верните обратно:

&НаСервере
Процедура ПересчитатьИтогиНаСервере()

	Дерево = РеквизитФормыВЗначение("ДеревоТоваров");

	Для Каждого Строка Из Дерево.Строки Цикл
		ПосчитатьИтог(Строка);
	КонецЦикла;

	ЗначениеВРеквизитФормы(Дерево, "ДеревоТоваров");

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

После ЗначениеВРеквизитФормы развёрнутые узлы на форме сворачиваются. Если пользователю важно сохранить раскрытие, проще пересчитать данные прямо через ПолучитьЭлементы(), не пересоздавая реквизит.

Практические задачи: удаление строк и выгрузка в плоскую таблицу

Удалить строки при обходе

Удалять строки внутри Для Каждого по той же коллекции нельзя: после удаления следующий элемент сдвигается, и цикл его пропускает. Идите с конца коллекции:

Процедура УдалитьПустые(КоллекцияСтрок)

	Для Сч = 1 По КоллекцияСтрок.Количество() Цикл
		Строка = КоллекцияСтрок[КоллекцияСтрок.Количество() - Сч];

		УдалитьПустые(Строка.Строки);

		Если Строка.Строки.Количество() = 0 И Строка.Сумма = 0 Тогда
			КоллекцияСтрок.Удалить(Строка);
		КонецЕсли;
	КонецЦикла;

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

Сначала чистятся дети, потом проверяется сам узел. Поэтому группа, у которой удалили все товары, тоже уйдёт, если её сумма нулевая.

Граница Для Сч = 1 По КоллекцияСтрок.Количество() вычисляется один раз при входе в цикл, а индекс Количество() - Сч берётся от текущего размера. Удаление строки с конца не сдвигает ещё не пройденные строки, поэтому индекс всегда указывает на нужную.

Выгрузить конечные строки в таблицу значений

Для загрузки в табличную часть документа обычно нужны только конечные строки — без групп:

Процедура ВыгрузитьКонечные(КоллекцияСтрок, Таблица)

	Для Каждого Строка Из КоллекцияСтрок Цикл
		Если Строка.Строки.Количество() = 0 Тогда
			ЗаполнитьЗначенияСвойств(Таблица.Добавить(), Строка);
		Иначе
			ВыгрузитьКонечные(Строка.Строки, Таблица);
		КонецЕсли;
	КонецЦикла;

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

ЗаполнитьЗначенияСвойств переносит значения колонок с одинаковыми именами, так что таблицу достаточно создать с нужными колонками.

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

  • ❌ Цикл только по Дерево.Строки — вложенные строки не обработаны.
  • ❌ Обращение к .Строки у ДанныеФормыДерево — на форме нужен ПолучитьЭлементы().
  • ❌ Попытка передать ДеревоЗначений на клиент параметром — этот тип там недоступен, нужно преобразование в реквизит формы.
  • ❌ Добавление строк в коллекцию, по которой сейчас идёт Для Каждого: результат непредсказуем, новые строки собирайте в отдельный массив и добавляйте после цикла.

Частые вопросы

Как узнать уровень вложенности строки дерева значений?
Вызовите метод Строка.Уровень(). Строки верхнего уровня возвращают 0, их дети — 1 и так далее.
Как получить родителя строки верхнего уровня?
Никак: у строк верхнего уровня свойство Родитель равно Неопределено. Это удобно как условие остановки при подъёме к корню.
Можно ли отсортировать дерево сразу на всех уровнях?
Да. У метода Сортировать коллекции строк второй параметр включает сортировку подчинённых строк: Дерево.Строки.Сортировать("Наименование", Истина).
Чем отличается выгрузка запроса ПоГруппировкам от ПоГруппировкамСИерархией?
Первый вариант строит уровни только по группировкам из ИТОГИ ПО. Второй дополнительно разворачивает иерархию справочника, если в итогах указано ИЕРАРХИЯ, — группы справочника станут отдельными уровнями дерева.
Как посчитать количество всех строк дерева, включая вложенные?
Отдельного свойства для этого нет: Дерево.Строки.Количество() вернёт только верхний уровень. Пройдите дерево рекурсией или стеком со счётчиком или используйте НайтиСтроки по служебной колонке и возьмите размер массива.