Работа с иерархическими структурами данных, такими как ДеревоЗначений, является одной из базовых задач программиста 1С. Рекурсия идеально подходит для обхода или заполнения таких структур, однако она требует предельной внимательности к условиям выхода и передаче параметров. Разберем на конкретном примере, как построить дерево заданной глубины, избежав типичных ошибок начинающих разработчиков.
Рассмотрим ситуацию, с которой столкнулся автор темы. Перед ним стояла задача: заполнить дерево значений случайными числами на определенную глубину. В исходном коде была допущена логическая ошибка в организации циклов и рекурсивных вызовов. Проанализируем фрагмент, который приводил к неправильному результату:
Процедура ЗаполнитьГраф(ТекущийУзел, Знач Глубина)
ГСЧ = Новый ГенераторСлучайныхЧисел();
Глубина = Глубина - 1;
Если НЕ Глубина > 0 Тогда
Возврат;
КонецЕсли;
Для Каждого Узел Из ТекущийУзел.Строки Цикл
Для Счетчик = 1 По 2 Цикл
НовыйУзел = Узел.Строки.Добавить();
НовыйУзел.Значение = ГСЧ.СлучайноеЧисло(1, 100);
ЗаполнитьГраф(Узел, Глубина);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
В чем заключаются основные ошибки этого подхода?
Для Каждого Узел Из ТекущийУзел.Строки. Если мы вызываем процедуру для пустого дерева, этот цикл просто не выполнится ни разу. Если же в дереве уже есть строки, код пытается добавить в каждую из них по два потомка и тут же рекурсивно уйти вглубь. Это создает путаницу в уровнях.ГенераторСлучайныхЧисел внутри рекурсии — плохая практика. Поскольку инициализация часто привязана к системному времени, при быстрых вызовах генератор может выдавать одинаковые последовательности чисел.Если НЕ Глубина > 0 срабатывает слишком рано или поздно в зависимости от того, как передается параметр.Для решения задачи нам необходимо разделить создание колонок и само заполнение. Выясним, как организовать процесс по шагам. Сначала подготовим само ДеревоЗначений и добавим в него необходимые колонки.
// Подготовительный этап
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Значение", Новый ОписаниеТипов("Число"));
// Создаем генератор один раз
ГСЧ = Новый ГенераторСлучайныхЧисел();
// Запускаем процесс для корня дерева
ЗаполнитьРекурсивно(Дерево, 3, ГСЧ);
Теперь спроектируем саму рекурсивную процедуру. Мы будем передавать в нее родительский узел (это может быть само ДеревоЗначений или СтрокаДереваЗначений) и текущий уровень глубины.
Процедура ЗаполнитьРекурсивно(Родитель, Глубина, ГСЧ)
// 1. Проверяем условие выхода
Если Глубина = 0 Тогда
Возврат;
КонецЕсли;
// 2. Создаем ветвление (например, по 2 узла на каждом уровне)
Для Итератор = 1 По 2 Цикл
// Добавляем новую строку в коллекцию строк родителя
НоваяСтрока = Родитель.Строки.Добавить();
// Присваиваем случайное значение
НоваяСтрока.Значение = ГСЧ.СлучайноеЧисло(1, 100);
// 3. Рекурсивный вызов для только что созданной строки
// Уменьшаем глубину для следующего уровня
ЗаполнитьРекурсивно(НоваяСтрока, Глубина - 1, ГСЧ);
КонецЦикла;
КонецПроцедуры
При работе с деревьями значений важно учитывать контекст выполнения и системные ограничения платформы 1С.
Инициализация генератора случайных чисел
Как уже было отмечено, всегда создавайте ГенераторСлучайныхЧисел один раз на самом верхнем уровне вызова. Это гарантирует истинную "случайность" распределения данных по всему дереву. Если передача объекта через параметры кажется неудобной, его можно инициализировать в переменной модуля (на сервере).
Ограничение глубины рекурсии
В платформе 1С существует лимит на стек вызовов. Обычно это около 1000 уровней. Если ваша бизнес-логика предполагает дерево глубиной 5000 уровней, рекурсия вызовет аварийное завершение работы сеанса. В таких случаях следует использовать итеративный алгоритм с использованием массива (стека).
Типизация колонок
Всегда явно указывайте типы колонок при создании ДеревоЗначений. Если колонка не типизирована, это может привести к ошибкам при отображении данных в управляемых формах или при выполнении математических операций с узлами.
Рассмотрим ситуацию, когда нам нужно избежать рекурсии. Проанализируем алгоритм "в ширину", используя массив как очередь задач. Это более безопасный способ заполнения огромных структур данных.
Процедура ЗаполнитьИтеративно(Дерево, МаксГлубина)
ГСЧ = Новый ГенераторСлучайныхЧисел();
// Очередь будет хранить структуры с родителем и его текущим уровнем
Очередь = Новый Массив;
Очередь.Добавить(Новый Структура("Узел, Уровень", Дерево, 0));
Пока Очередь.Количество() > 0 Цикл
ТекущаяЗадача = Очередь[0];
Очередь.Удалить(0);
Если ТекущаяЗадача.Уровень < МаксГлубина Тогда
Для Счетчик = 1 По 2 Цикл
НоваяСтрока = ТекущаяЗадача.Узел.Строки.Добавить();
НоваяСтрока.Значение = ГСЧ.СлучайноеЧисло(1, 100);
// Добавляем новых детей в очередь для обработки
Очередь.Добавить(Новый Структура("Узел, Уровень", НоваяСтрока, ТекущаяЗадача.Уровень + 1));
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот метод исключает риск переполнения стека и часто работает более предсказуемо при отладке, так как мы видим состояние очереди в каждый момент времени.
Для проверки правильности заполнения структуры в процессе разработки удобно использовать отладчик. Однако, если дерево большое, просматривать его в табло сложно. Рекомендуется временно выводить результат на форму в элемент типа Таблица, связанный с реквизитом ДанныеФормыДерево. Для этого используйте стандартный метод преобразования:
// В коде формы
ЗначениеВРеквизитФормы(Дерево, "РеквизитДеревоНаФорме");
Резюмируем: при создании иерархий через рекурсию всегда контролируйте три вещи: точку входа (правильный родительский узел), шаг изменения параметра (уменьшение глубины) и условие выхода. Правильно спроектированный алгоритм позволит вам создавать сложные динамические отчеты и структуры данных для любых задач автоматизации.