Как перебрать все строки дерева значений в 1С и не сломать иерархию

· 10 мин

Содержание

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

Типичная ситуация: запрос с ИТОГИ ПО Контрагент, Договор выгрузили в дерево, отчёт печатает только контрагентов, а договоры и документы пропали. Причина почти всегда одна: правильно обойти дерево значений нужно по уровням, а код перебирает одну коллекцию. Ниже — рабочие приёмы с кодом: перебор, поиск, итоги, изменение структуры и работа с деревом на управляемой форме.

Как устроено дерево значений: строки, колонки, родители

У ДеревоЗначений два главных свойства:

  • 📋 Колонки — одна коллекция колонок на всё дерево. Колонки общие для всех уровней.
  • 🌳 Строки — коллекция строк верхнего уровня (КоллекцияСтрокДереваЗначений).

Каждая строка (СтрокаДереваЗначений) хранит значения колонок и ещё три вещи, которые нужны при обходе:

ЧтоЧто возвращает
Строка.СтрокиКоллекцию подчинённых строк, такую же, как Дерево.Строки
Строка.РодительСтроку-родителя; у верхнего уровня — Неопределено
Строка.Уровень()Номер уровня, верхний уровень — 0

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

Дерево создаётся двумя способами. Вручную:

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

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

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

Или из запроса с итогами:

Результат = Запрос.Выполнить();
Дерево = Результат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Задавайте колонкам тип через ОписаниеТипов. Без типа колонка хранит что угодно, и сложение Неопределено + 100 упадёт с ошибкой, а метод Итог() для такой колонки не сработает.

ДеревоЗначений — серверный объект. На клиенте тонкого клиента его нет, там работают с данными формы (об этом — в отдельном разделе ниже).

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

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

Функция ВсеСтрокиДерева(Дерево)

	Результат = Новый Массив;
	Стек = Новый Массив;
	Стек.Добавить(Дерево.Строки);

	Пока Стек.Количество() > 0 Цикл
		Коллекция = Стек[Стек.ВГраница()];
		Стек.Удалить(Стек.ВГраница());

		Для Каждого Строка Из Коллекция Цикл
			Результат.Добавить(Строка);
			Если Строка.Строки.Количество() > 0 Тогда
				Стек.Добавить(Строка.Строки);
			КонецЕсли;
		КонецЦикла;
	КонецЦикла;

	Возврат Результат;

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

Функция возвращает плоский массив всех строк. Дальше с ним работают обычным циклом:

Для Каждого Строка Из ВсеСтрокиДерева(Дерево) Цикл
	Строка.Сумма = Окр(Строка.Сумма * 1.1, 2);
КонецЦикла;

Когда этот вариант лучше рекурсии:

  • 🔁 дерево очень глубокое, и рекурсия рискует переполнить стек встроенного языка;
  • 📦 строки нужно обработать несколько раз — массив собрали один раз и переиспользуете;
  • 🧩 обработку удобно вынести в другой модуль и передать туда готовый массив.

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

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

Рекурсия даёт естественный порядок: родитель, все его потомки, следующий родитель. Так дерево выглядит на экране, так его и печатают.

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

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

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

		// Спуск на уровень ниже
		ОбойтиСтроки(Строка.Строки);

	КонецЦикла;

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

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

Процедура принимает именно коллекцию строк, а не дерево. Поэтому один и тот же код работает и для верхнего уровня (Дерево.Строки), и для любой ветки (Строка.Строки).

Обработка «после детей»

Если результат родителя зависит от потомков (итоги, проверка «все ли дети заполнены»), переставьте вызов: сначала рекурсия, потом обработка строки. Это обход в обратном порядке — снизу вверх.

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

Обход только до нужной глубины

Передайте в процедуру предельный уровень и не спускайтесь глубже:

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

С МаксУровень = 1 обработаются строки уровней 0 и 1, например, контрагенты и их договоры без документов.

Обход дерева из запроса без выгрузки

Если дерево нужно только для чтения, его можно не создавать. Выборка по группировкам обходит результат запроса так же иерархично и расходует меньше памяти:

ВыборкаКонтрагенты = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаКонтрагенты.Следующий() Цикл
	ВыборкаДетали = ВыборкаКонтрагенты.Выбрать();
	Пока ВыборкаДетали.Следующий() Цикл
		// строки внутри контрагента
	КонецЦикла;
КонецЦикла;

Поиск строки в дереве значений: Найти и НайтиСтроки

Писать свою рекурсию ради поиска не нужно. У коллекции строк есть два метода, и оба умеют искать по всем уровням.

МетодЧто вернётКогда применять
Найти(Значение, Колонки, ВключатьПодчиненные)Первую найденную строку или НеопределеноНужна одна строка по одному значению
НайтиСтроки(Отбор, ВключатьПодчиненные)Массив строк (может быть пустым)Нужны все совпадения или отбор по нескольким колонкам

Параметр ВключатьПодчиненные по умолчанию равен Ложь. Без Истина поиск идёт только по той коллекции, у которой вызван метод.

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

// Все строки с отбором по двум колонкам
Отбор = Новый Структура("Склад, Организация", Склад, Организация);
МассивСтрок = Дерево.Строки.НайтиСтроки(Отбор, Истина);

Колонки в Найти перечисляют через запятую строкой. Если колонки не указать, поиск пройдёт по всем.

Путь от найденной строки до корня

Часто строку нашли, а нужно понять, в какой она группе. Поднимайтесь по Родитель, пока не получите Неопределено:

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

Получится строка вида Поставщики / ООО Ромашка / Договор № 12.

Много поисков подряд

У дерева, в отличие от таблицы значений, нет индексов. Если в цикле по 10 000 документов вы каждый раз вызываете Найти(..., Истина), каждый вызов заново проходит всё дерево. Один раз соберите Соответствие «ключ → строка», а дальше берите строку по ключу:

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

СтрокаТовара = КартаСтрок.Получить(Товар);

Итоги по узлам дерева: сумма снизу вверх

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

Функция ЗаполнитьИтоги(КоллекцияСтрок)

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

	Возврат Всего;

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

ИтогПоДереву = ЗаполнитьИтоги(Дерево.Строки);

Проверим на примере. Группа «Поставщики» содержит две строки: 15 000 и 7 500. Группа «Покупатели» — одну строку 4 200. После вызова у «Поставщиков» будет 22 500, у «Покупателей» 4 200, а ИтогПоДереву = 26 700.

Тот же каркас подходит для других агрегатов:

  • 🔢 количество листьев: прибавляйте 1 только для строк без потомков;
  • 📈 максимум по ветке: вместо суммы храните наибольшее значение;
  • ✅ признак «ветка заполнена»: родитель истинен, только если истинны все дети.

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

Если дерево получено из запроса с ИТОГИ СУММА(Сумма) ПО ..., итоги в группировках уже посчитаны платформой. Пересчитывать их не нужно, если вы не меняли суммы в деталях.

Добавление, перенос и удаление строк при обходе дерева

Добавление

Новую строку добавляют в коллекцию того уровня, где она нужна:

  • Дерево.Строки.Добавить() — на верхний уровень;
  • Родитель.Строки.Добавить() — в конец ветки;
  • Родитель.Строки.Вставить(0) — в начало ветки.

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

Удаление: только с конца

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

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

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

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

Индекс идёт от Всего - 1 до 0. Сначала чистятся потомки, поэтому группа, у которой удалили всех детей, тоже удалится, если её сумма нулевая.

Удаление строки удаляет и всю её ветку. Отдельно чистить потомков перед этим не нужно.

Перенос ветки к другому родителю

Метода «сменить родителя» у строки дерева нет. Ветку копируют рекурсивно в новое место, а старую удаляют:

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

СкопироватьВетку(СтрокаДляПереноса, НовыйРодитель.Строки);

СтарыйРодитель = СтрокаДляПереноса.Родитель;
Если СтарыйРодитель = Неопределено Тогда
	Дерево.Строки.Удалить(СтрокаДляПереноса);
Иначе
	СтарыйРодитель.Строки.Удалить(СтрокаДляПереноса);
КонецЕсли;

Не переносите ветку внутрь её же потомка. Копия попадёт в копируемую ветку, и рекурсия станет бесконечной. Перед переносом поднимитесь от НовыйРодитель по Родитель и убедитесь, что СтрокаДляПереноса на этом пути не встречается.

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

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

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

На сервере (ДеревоЗначений)На форме (ДанныеФормыДерево)
Дерево.СтрокиДерево.ПолучитьЭлементы()
Строка.СтрокиЭлемент.ПолучитьЭлементы()
Строка.РодительЭлемент.ПолучитьРодителя()

Рекурсия на клиенте пишется так же, только через ПолучитьЭлементы(). Пример — команда «Развернуть всё»:

&НаКлиенте
Процедура РазвернутьВсе(Команда)
	Для Каждого Элемент Из Дерево.ПолучитьЭлементы() Цикл
		Элементы.Дерево.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
	КонецЦикла;
КонецПроцедуры

Второй параметр Истина раскрывает строку со всеми подчинёнными, поэтому достаточно пройти верхний уровень.

Для тяжёлой обработки переведите реквизит в обычное дерево на сервере, а результат верните обратно:

&НаСервере
Процедура ПересчитатьНаСервере()
	ДеревоЗн = РеквизитФормыВЗначение("Дерево");
	ЗаполнитьИтоги(ДеревоЗн.Строки);
	ЗначениеВРеквизитФормы(ДеревоЗн, "Дерево");
КонецПроцедуры

Так работают все методы ДеревоЗначений, включая Найти и НайтиСтроки с поиском по уровням.

Ошибки и скорость обхода дерева значений

Что чаще всего ломает обход и как это распознать:

Диагностика: выберите симптом

Что ускоряет обход на больших деревьях:

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

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

Чем дерево значений отличается от таблицы значений при обходе?
У таблицы одна плоская коллекция строк, и одного цикла хватает. У дерева коллекции вложены, поэтому нужен спуск по Строка.Строки. Зато у таблицы есть индексы, а у дерева их нет.
Можно ли превратить дерево значений в таблицу?
Готового метода нет. Создайте таблицу с теми же колонками, обойдите дерево рекурсией и для каждой строки добавьте строку в таблицу через ЗаполнитьЗначенияСвойств. Уровень можно сохранить в отдельную колонку через Строка.Уровень().
Как скопировать дерево значений целиком?
Метод Скопировать() у дерева возвращает новое дерево с теми же колонками и строками. Изменения в копии не затрагивают исходное дерево.
Как отсортировать дерево с учётом всех уровней?
Вызовите Дерево.Строки.Сортировать(“Наименование”, Истина). Второй параметр включает сортировку подчинённых строк, иначе упорядочится только верхний уровень.
Почему ДеревоЗначений недоступно в клиентской процедуре?
Это серверный тип. На клиенте с деревом на форме работают через ДанныеФормыДерево и метод ПолучитьЭлементы(), а сложную обработку выполняют на сервере.