Разработка алгоритмов обхода иерархических структур, деревьев значений, каталогов файлов или генерации всех возможных комбинаций из нескольких наборов данных — классическая задача в программировании. В среде 1С:Предприятие традиционным подходом часто выступает рекурсия (вызов функции из самой себя). Однако рекурсивные вызовы обладают критическим недостатком: глубина системного стека вызовов в платформе жестко ограничена (обычно лимит составляет от 1000 до 2000 вызовов в зависимости от разрядности клиента и версии платформы). При превышении этого лимита происходит аварийное закрытие приложения без возможности перехвата ошибки через конструкцию Попытка...Исключение.
Для создания надежных, отказоустойчивых и высокопроизводительных решений разработчикам необходимо уметь заменять рекурсию явными итерационными циклами. В этой статье мы подробно разберем четыре эффективных практических метода эмуляции многоуровневых вложенных циклов в оперативной памяти и базах данных 1С.
Прежде чем перейти к алгоритмам, проанализируем, почему рекурсия приводит к падению системы, а циклы — нет. При каждом рекурсивном вызове операционная система выделяет память в специальной области — стеке вызовов. Там хранятся локальные переменные функции и адрес возврата. Объем стековой памяти крайне мал.
Если мы переписываем алгоритм на обычный цикл Пока или Для, мы переносим хранение состояния итераций из системного стека в кучу (heap) — общую оперативную память процесса. В 1С куча используется для хранения коллекций, таких как Массив, СписокЗначений или ТаблицаЗначений. Объем кучи на современных 64-битных серверах и клиентах практически не ограничен и может измеряться гигабайтами. Таким образом, используя массивы в качестве структуры управления, мы можем обрабатывать миллионы уровней вложенности без риска падения программы.
Рассмотрим ситуацию, когда пользователю необходимо сгенерировать все возможные комбинации элементов из нескольких наборов (например, перебор характеристик товаров, вариантов настроек или решение логических задач). Количество свойств (уровней вложенности) заранее неизвестно.
Для решения этой задачи идеально подходит алгоритм, имитирующий работу механического счетчика километров в автомобиле (одометра). Мы создаем один единственный цикл Пока, а переходом по уровням вложенности управляем вручную с помощью массива индексов.
Разберем по шагам практическую реализацию этого алгоритма:
// Определяем количество уровней вложенности (задается динамически)
КоличествоУровней = 4;
// Массив, хранящий максимальное количество элементов на каждом уровне
ГраницыРазрядов = Новый Массив;
ГраницыРазрядов.Добавить(3); // Например, на 1-м уровне 3 элемента
ГраницыРазрядов.Добавить(2); // На 2-м уровне 2 элемента
ГраницыРазрядов.Добавить(4); // На 3-м уровне 4 элемента
ГраницыРазрядов.Добавить(3); // На 4-м уровне 3 элемента
// Массив текущих индексов (наш одометр), заполняем нулями
ТекущиеИндексы = Новый Массив;
Для Счетчик = 1 По КоличествоУровней Цикл
ТекущиеИндексы.Добавить(0);
КонецЦикла;
ВсегоКомбинаций = 0;
Пока Истина Цикл
// --- БЛОК ОБРАБОТКИ ТЕКУЩЕЙ КОМБИНАЦИИ ---
// В этой точке ТекущиеИндексы содержат уникальную комбинацию.
// Например: [0, 0, 0, 0], на следующем шаге [0, 0, 0, 1] и т.д.
ВсегоКомбинаций = ВсегоКомбинаций + 1;
СтрокаЛога = "Комбинация № " + ВсегоКомбинаций + ": ";
Для Каждого Индекс Из ТекущиеИндексы Цикл
СтрокаЛога = СтрокаЛога + Индекс + " | ";
КонецЦикла;
ЗаписьЖурналаРегистрации("ПереборКомбинаций", УровеньЖурналаРегистрации.Информация,,, СтрокаЛога);
// ----------------------------------------
// Инкрементируем младший разряд (имитируем правое колесико счетчика)
ИндексТекущегоРазряда = КоличествоУровней - 1;
Пока ИндексТекущегоРазряда >= 0 Цикл
// Увеличиваем значение в текущем разряде на единицу
ТекущиеИндексы[ИндексТекущегоРазряда] = ТекущиеИндексы[ИндексТекущегоРазряда] + 1;
// Проверяем, не достигли ли мы предела для данного разряда
Если ТекущиеИндексы[ИндексТекущегоРазряда] < ГраницыРазрядов[ИндексТекущегоРазряда] Тогда
// Перенос разряда не требуется, прерываем внутренний цикл инкремента
Прервать;
Иначе
// Достигли предела: сбрасываем текущий разряд в 0
// и переносим единицу на разряд старше (влево)
ТекущиеИндексы[ИндексТекущегоРазряда] = 0;
ИндексТекущегоРазряда = ИндексТекущегоРазряда - 1;
КонецЕсли;
КонецЦикла;
// Если индекс разряда ушел в отрицательную область, значит,
// мы совершили перенос старшего разряда. Перебор полностью завершен.
Если ИндексТекущегоРазряда < 0 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Преимущества метода: Работает чрезвычайно быстро, потребляет константный объем памяти, не зависящий от количества комбинаций, и легко адаптируется под любые типы данных.
В бизнес-приложениях часто требуется обойти иерархическую структуру «сверху вниз»: например, прочитать все подчиненные группы и элементы справочника Номенклатура, выгрузить структуру каталогов на диске или обойти дерево отборов компоновки данных.
Для этого применяется обход в ширину (Breadth-First Search). При таком подходе мы сначала обрабатываем все элементы текущего уровня, затем все элементы следующего уровня и так далее.
В качестве управляющей структуры мы будем использовать обычный одномерный Массив в роли очереди (FIFO - First In, First Out). Посмотрим на реализацию этого паттерна:
// Предположим, у нас есть корневой элемент дерева (например, группа номенклатуры)
КорневаяСсылка = Справочники.Номенклатура.НайтиПоКоду("00000001");
ОчередьОбхода = Новый Массив;
ОчередьОбхода.Добавить(КорневаяСсылка);
// Вместо удаления элементов из начала массива (что медленно в 1С из-за сдвига индексов)
// мы будем использовать указатель текущей позиции
ТекущийУказатель = 0;
Пока ТекущийУказатель < ОчередьОбхода.Количество() Цикл
// Извлекаем текущий элемент из очереди
ТекущийЭлемент = ОчередьОбхода[ТекущийУказатель];
ТекущийУказатель = ТекущийУказатель + 1;
// Выполняем полезную работу с элементом
Если ТекущийЭлемент.ЭтоГруппа Тогда
// Получаем подчиненные элементы (следующий уровень вложенности)
ВыборкаПодчиненных = Справочники.Номенклатура.Выбрать(ТекущийЭлемент);
Пока ВыборкаПодчиненных.Следующий() Цикл
// Динамически добавляем новые элементы в конец нашей очереди
ОчередьОбхода.Добавить(ВыборкаПодчиненных.Ссылка);
КонецЦикла;
Иначе
// Обработка конечного элемента (не группы)
ОбработатьКарточкуТовара(ТекущийЭлемент);
КонецЕсли;
КонецЦикла;
Важная особенность: Этот шаблон кода незаменим при написании алгоритмов в закрытых или труднодоступных модулях (например, в формулах, динамически выполняемых блоках через Выполнить() или в правилах конвертации данных «КД 2.0»), где невозможно объявить новую процедуру для классической рекурсии.
Если логика вашей задачи жестко требует сначала спуститься до самого глубокого уровня вложенности (до «листьев» дерева) и только потом переходить к соседним веткам (как это делает стандартная рекурсия), очередь заменяется на Стек (LIFO - Last In, First Out).
Мы эмулируем системный стек вызовов, используя Массив и методы работы с его концом (добавление элемента и удаление последнего индекса). Проанализируем пример кода:
СтекОбхода = Новый Массив;
// Кладем в стек начальный элемент структуры
СтекОбхода.Добавить(НачальныйУзелДерева);
Пока СтекОбхода.Количество() > 0 Цикл
// Получаем элемент с вершины стека (последний добавленный)
ИндексВершины = СтекОбхода.ВГраница();
ТекущийУзел = СтекОбхода[ИндексВершины];
// Извлекаем его из стека
СтекОбхода.Удалить(ИндексВершины);
// Обрабатываем узел
ВыполнитьДействиеСУзлом(ТекущийУзел);
// Если у узла есть вложенные элементы, добавляем их на вершину стека
КоллекцияПодчиненных = ТекущийУзел.Строки;
КоличествоПодчиненных = КоллекцияПодчиненных.Количество();
// Добавляем в обратном порядке, чтобы сохранить исходное направление обхода
Для Счетчик = 1 По КоличествоПодчиненных Цикл
ОбратныйИндекс = КоличествоПодчиненных - Счетчик;
СтекОбхода.Добавить(КоллекцияПодчиненных[ОбратныйИндекс]);
КонецЦикла;
КонецЦикла;
Этот способ позволяет полностью сохранить логику работы рекурсивной функции, но при этом гарантирует абсолютную устойчивость к переполнению стека.
Если задача сводится к перебору комбинаций, а исходные данные уже находятся в базе данных (или их можно поместить во временные таблицы), наиболее производительным решением в экосистеме 1С будет перенос вычислений на сторону СУБД.
Мы можем динамически сформировать текст запроса, который соединит таблицы «каждую с каждой» без условий связи (так называемый CROSS JOIN). Проанализируем, как это реализовать на практике:
// Массив временных таблиц, которые нужно перемножить
МассивИменВТ = Новый Массив;
МассивИменВТ.Добавить("ВТ_Цвета");
МассивИменВТ.Добавить("ВТ_Размеры");
МассивИменВТ.Добавить("ВТ_Материалы");
ТекстВыбора = "ВЫБРАТЬ ";
ТекстИсточников = "ИЗ ";
Для Индекс = 0 По МассивИменВТ.ВГраница() Цикл
ИмяВТ = МассивИменВТ[Индекс];
ИмяПсевдонима = "Т" + Индекс;
// Формируем поля выборки
ТекстВыбора = ТекстВыбора + ИмяПсевдонима + ".Значение КАК Параметр" + Индекс;
Если Индекс <> МассивИменВТ.ВГраница() Тогда
ТекстВыбора = ТекстВыбора + ", ";
КонецЕсли;
// Формируем таблицы источников через запятую для декартова произведения
ТекстИсточников = ТекстИсточников + ИмяВТ + " КАК " + ИмяПсевдонима;
Если Индекс <> МассивИменВТ.ВГраница() Тогда
ТекстИсточников = ТекстИсточников + ", ";
КонецЕсли;
КонецЦикла;
ЗапросКомбинаций = Новый Запрос;
ЗапросКомбинаций.МенеджерВременныхТаблиц = ПолучитьЗаполненныйМенеджерВТ();
ЗапросКомбинаций.Текст = ТекстВыбора + Символы.ПС + ТекстИсточников;
// На выходе получаем плоскую таблицу всех возможных комбинаций
ТаблицаРезультатов = ЗапросКомбинаций.Выполнить().Выгрузить();
// Теперь достаточно обойти результат ОДНИМ простым циклом
Для Каждого СтрокаКомбинации Из ТаблицаРезультатов Цикл
// Обработка готовой строки параметров
КонецЦикла;
Почему это эффективно? СУБД оптимизирована под работу с множествами. Перебор миллионов комбинаций в запросе выполнится в десятки раз быстрее, чем аналогичные итерации во встроенном языке 1С.
Выбор конкретного метода зависит от архитектуры вашей задачи:
Отказ от рекурсии в пользу явных циклов повышает надежность кода и гарантирует, что ваша программа не завершится аварийно при обработке больших массивов реальных данных.