При разработке на платформе 1С:Предприятие часто возникают задачи, связанные с обработкой иерархических данных. Одним из самых распространенных универсальных коллекций для хранения таких данных является ДеревоЗначений. В отличие от ТаблицаЗначений, дерево позволяет выстраивать подчиненность строк, создавая уровни вложенности любой глубины.
Основная сложность, с которой сталкиваются разработчики, — это необходимость обработать все строки дерева, независимо от того, на каком уровне иерархии они находятся. Простой цикл Для Каждого сработает только для первого уровня строк (корневых). Чтобы добраться до вложенных элементов, необходимо использовать рекурсию.
В этой статье мы подробно разберем, как написать алгоритм рекурсивного обхода, рассмотрим примеры кода и проанализируем важные нюансы работы с коллекциями строк.
Представим, что у нас есть объект типа ДеревоЗначений. Оно может быть сформировано программно, получено из результата запроса с итогами или загружено из внешнего файла. Наша цель — пройти по всем элементам дерева, начиная с корня и заканчивая самыми глубокими "листьями", и выполнить над ними определенное действие (например, проверить заполнение реквизитов, пересчитать суммы или изменить значения).
Рассмотрим структуру, с которой нам предстоит работать. У самого объекта ДеревоЗначений есть коллекция Строки. Это строки первого (верхнего) уровня. У каждой строки этой коллекции (объект СтрокаДереваЗначений) также есть собственное свойство Строки, которое содержит подчиненные элементы. И так далее до бесконечности.
Рекурсия в программировании — это ситуация, когда процедура или функция вызывает саму себя. Для обхода дерева это идеальный подход. Логика алгоритма выглядит следующим образом:
Давайте посмотрим, как это реализуется на языке 1С. Разберем базовый шаблон процедуры.
Создадим процедуру, назовем её ОбойтиКоллекциюСтрок. В качестве параметра она будет принимать коллекцию типа КоллекцияСтрокДереваЗначений. При передаче коллекции важно учитывать особенности использования "Знач".
Рассмотрим код решения:
Процедура ОбойтиКоллекциюСтрок(КоллекцияСтрок)
// Перебираем текущий уровень строк
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
// 1. Выполняем необходимые действия с текущей строкой
// Например, выведем значение колонки "Наименование" в журнал регистрации или изменим его
// Сообщить("Обработка строки: " + СтрокаДерева.Наименование);
// Здесь может быть любой ваш код:
// СтрокаДерева.Сумма = СтрокаДерева.Количество * СтрокаДерева.Цена;
// 2. Рекурсивный вызов для подчиненных строк
// Проверяем, есть ли подчиненные строки (не обязательно, цикл просто не выполнится, но для наглядности полезно)
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ОбойтиКоллекциюСтрок(СтрокаДерева.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Чтобы запустить этот процесс, нам нужно вызвать эту процедуру, передав ей строки самого верхнего уровня нашего дерева:
// Предположим, переменная ДеревоДанных - это наше ДеревоЗначений
ОбойтиКоллекциюСтрок(ДеревоДанных.Строки);
Разберем детально, что происходит при выполнении этого кода:
ДеревоДанных.Строки.ОбойтиКоллекциюСтрок(СтрокаДерева.Строки) "проваливается" внутрь первой строки.Если вы работаете в Управляемом приложении (Тонкий клиент, Веб-клиент), то вы, скорее всего, взаимодействуете не с ДеревоЗначений, а с реквизитом формы типа ДанныеФормыДерево. На формах также часто приходится настраивать отборы динамического списка для иерархических данных.
Логика работы остается абсолютно такой же, однако тип коллекции будет ДанныеФормыКоллекцияЭлементовДерева. Платформа 1С позволяет использовать тот же самый алгоритм обхода для данных формы, так как у элементов коллекции данных формы тоже есть свойство Строки (точнее, метод ПолучитьЭлементы() или обращение через свойство, в зависимости от контекста, но чаще всего структура идентична дереву значений).
Рассмотрим пример для управляемой формы, где нам нужно посчитать сумму в колонке Сумма для всех строк дерева, включая вложенные, на клиенте.
&НаКлиенте
Процедура ПересчитатьСуммыВДевеве(ЭлементыДерева)
Для Каждого ТекСтрока Из ЭлементыДерева Цикл
// Полезное действие
Если ТекСтрока.Цена <> 0 И ТекСтрока.Количество <> 0 Тогда
ТекСтрока.Сумма = ТекСтрока.Цена * ТекСтрока.Количество;
КонецЕсли;
// Рекурсия: получаем подчиненные элементы
ПодчиненныеЭлементы = ТекСтрока.ПолучитьЭлементы();
ПересчитатьСуммыВДевеве(ПодчиненныеЭлементы);
КонецЦикла;
КонецПроцедуры
// Кнопка запуска на форме
&НаКлиенте
Процедура КомандаПересчитать(Команда)
// ДеревоОбъектов - это имя реквизита формы типа ДеревоЗначений
ПересчитатьСуммыВДевеве(ДеревоОбъектов.ПолучитьЭлементы());
КонецПроцедуры
Обратите внимание на метод ПолучитьЭлементы(). Для объектов ДанныеФормыДерево (реквизит формы) это основной способ доступа к коллекции строк верхнего уровня, а для ДанныеФормыЭлементДерева (строка) — способ доступа к подчиненным строкам.
Еще одна частая задача — "развернуть" дерево в плоскую таблицу значений. Например, пользователь заполнил иерархический список товаров, а нам для проведения документа нужно получить простую таблицу без папок и групп.
Для этого мы можем модифицировать нашу процедуру, добавив в параметры таблицу-приемник.
// Процедура для сбора данных из дерева в таблицу
Процедура СобратьДеревоВТаблицу(КоллекцияСтрок, ТаблицаПриемник)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
// Добавляем новую строку в плоскую таблицу
НоваяСтрока = ТаблицаПриемник.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева);
// Рекурсивно идем вглубь
СобратьДеревоВТаблицу(СтрокаДерева.Строки, ТаблицаПриемник);
КонецЦикла;
КонецПроцедуры
Посмотрим на пример использования:
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Товар");
ТаблицаРезультат.Колонки.Добавить("Количество");
// ... добавляем нужные колонки
СобратьДеревоВТаблицу(ДеревоТоваров.Строки, ТаблицаРезультат);
// Теперь ТаблицаРезультат содержит все строки дерева в одном плоском списке
Иногда требуется более сложная логика: например, посчитать сумму в группировках, основываясь на суммах подчиненных элементов. Для этого действие над строкой нужно выполнять после того, как обработаны все подчиненные элементы (возврат из рекурсии).
Рассмотрим, как изменить алгоритм для подсчета итогов по иерархии:
Процедура РассчитатьИтогиПоИерархии(КоллекцияСтрок)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
// Сначала идем вглубь! Обрабатываем детей.
РассчитатьИтогиПоИерархии(СтрокаДерева.Строки);
// После возврата из рекурсии данные в подчиненных строках уже готовы (посчитаны)
// Если у строки есть подчиненные, значит это группа - считаем по ней итог
Если СтрокаДерева.Строки.Количество() > 0 Тогда
СтрокаДерева.Сумма = 0;
Для Каждого ПодчиненнаяСтрока Из СтрокаДерева.Строки Цикл
СтрокаДерева.Сумма = СтрокаДерева.Сумма + ПодчиненнаяСтрока.Сумма;
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
В данном примере код расчета выполняется при "высплывании" из глубины рекурсии. Это гарантирует, что к моменту расчета суммы родителя, суммы всех его детей (и внуков) уже актуализированы.
При работе с рекурсией и деревьями стоит помнить о нескольких важных моментах:
Для Каждого, это приведет к ошибке или некорректной работе итератора. Для удаления используйте обратный цикл Для Сч = -Количество() По 1 Шаг -1 или собирайте строки к удалению в отдельный массив. В контексте рекурсии это тоже актуально.Используя приведенные шаблоны, вы сможете эффективно решать любые задачи по обработке иерархических данных в 1С:Предприятие 8, будь то сложные отчеты, загрузка данных из XML/JSON или расчет себестоимости узлов.