Рекурсия в 1С:Предприятие — это мощный инструмент, который позволяет решать задачи с повторяющейся логикой, работая с вложенными структурами данных, такими как деревья справочников, иерархии документов или многоуровневые отчеты. Если вы когда-нибудь сталкивались с необходимостью обойти все подчиненные элементы справочника, построить дерево подчинённости или рассчитать итоги по вложенным группам — рекурсия станет вашим незаменимым помощником.
В этой статье мы разберём, что такое рекурсия с точки зрения платформы 1С 8.3, как она реализуется на встроенном языке, где её применение оправдано, а где лучше использовать альтернативные подходы. Вы узнаете о типичных ошибках начинающих разработчиков, научитесь оптимизировать рекурсивные алгоритмы и избегать зацикливания. Особое внимание уделим практическим примерам — от простейшего факториала до сложных обходов иерархических структур.
Материал будет полезен как новичкам, которые только начинают осваивать программирование в 1С, так и опытным специалистам, желающим систематизировать знания или найти нестандартные решения для своих задач.
Что такое рекурсия и как она работает в 1С
Рекурсия — это приём программирования, при котором функция или процедура вызывает саму себя непосредственно или через цепочку других вызовов. В контексте 1С:Предприятие рекурсия реализуется через встроенный язык и позволяет эффективно работать с данными, имеющими иерархическую или вложенную структуру.
Основные компоненты рекурсивной функции:
- 🔹 Базовый случай — условие, при котором рекурсия прекращается. Без него функция будет вызывать себя бесконечно, что приведёт к переполнению стека и ошибке.
- 🔹 Рекурсивный случай — часть кода, где функция вызывает саму себя с изменёнными параметрами, приближаясь к базовому случаю.
- 🔹 Параметры — данные, которые передаются между вызовами и модифицируются для достижения базового случая.
В 1С 8.3 рекурсия поддерживается на уровне языка, но имеет ограничения, связанные с глубиной стека вызовов (по умолчанию — около 1000 уровней). Превышение этого лимита приводит к исключению Переполнение стека. Например, при обходе глубокой иерархии справочников может потребоваться увеличение этого параметра через настройки конфигуратора.
Простейший пример рекурсии — расчёт факториала числа. В математической нотации факториал числа n (обозначается как n!) равен произведению всех целых чисел от 1 до n. Рекурсивное решение этой задачи в 1С выглядит так:
Функция Факториал(Число) Экспорт
Если Число = 1 Тогда
Возврат 1; // Базовый случай
Иначе
Возврат Число * Факториал(Число - 1); // Рекурсивный вызов
КонецЕсли;
КонецФункции
Этот код демонстрирует классическую структуру рекурсии: проверка базового случая и рекурсивный вызов с уменьшением параметра. Однако в реальных задачах 1С рекурсия чаще применяется для работы с объектами метаданных, а не для математических вычислений.
Где применяется рекурсия в 1С: реальные примеры
Рекурсия в 1С:Предприятие находит применение в следующих сценариях:
- 📁 Обход иерархических справочников: построение деревьев подчинённости, расчёт итогов по группам, поиск элементов по всему дереву.
- 📄 Обработка вложенных документов: например, расчёт себестоимости по многоуровневым спецификациям или обход цепочки связанных заказов.
- 📊 Формирование отчётов с группировками: когда данные имеют вложенную структуру (например, отчёт по подразделениям с подчиненными отделами).
- 🔄 Рекурсивные алгоритмы обработки: поиск циклов в графах, обход лабиринтов (например, в задачах логистики).
- 🔧 Работа с XML/JSON: парсинг вложенных структур данных, когда глубина вложенности заранее неизвестна.
Рассмотрим практический пример: обход всех элементов справочника Номенклатура с учётом иерархии групп. Допустим, нам нужно получить список всех позиций номенклатуры, включая вложенные группы, для дальнейшей обработки:
Процедура ОбойтиНоменклатуру(Группа, МассивРезультатов) Экспорт
// Получаем все элементы текущей группы
Выборка = Справочники.Номенклатура.Выбрать(,, Группа);
Пока Выборка.Следующий() Цикл
Если Выборка.ЭтоГруппа Тогда
ОбойтиНоменклатуру(Выборка.Ссылка, МассивРезультатов); // Рекурсия для вложенных групп
Иначе
МассивРезультатов.Добавить(Выборка.Ссылка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот код рекурсивно обходит все уровни справочника, добавляя негрупповые элементы в результирующий массив. Аналогичный подход применяется для обхода документов, регистров сведений или любых других объектов с иерархией.
При обходе больших справочников (тысячи элементов) рекурсия может привести к переполнению стека. В таких случаях лучше использовать итеративный подход с очередью (стеком) для хранения обрабатываемых узлов.
Типичные ошибки при работе с рекурсией в 1С
Несмотря на мощь рекурсии, её неправильное использование может привести к серьёзным проблемам в коде. Вот наиболее распространённые ошибки, которые допускают разработчики 1С:
- Отсутствие базового случая — функция бесконечно вызывает саму себя, что приводит к зависанию программы или ошибке переполнения стека. Например:
Функция БесконечнаяРекурсия(Параметр)
Возврат БесконечнаяРекурсия(Параметр + 1); // Нет условия выхода!
КонецФункции
- Неправильная модификация параметров — если параметры не изменяются или изменяются некорректно, рекурсия никогда не достигнет базового случая. Например, вместо уменьшения параметра его увеличивают.
- Чрезмерная глубина рекурсии — в 1С по умолчанию ограничение на глубину стека вызовов составляет ~1000 уровней. При обходе глубоких структур (например, справочник с 2000 уровней вложенности) это приведёт к ошибке.
- Потеря контекста — при рекурсивных вызовах легко забыть, какие данные передаются между уровнями, что приводит к некорректным результатам.
- Игнорирование производительности — рекурсия может быть менее эффективной, чем итеративные алгоритмы, особенно при работе с большими объёмами данных.
Пример ошибки с отсутствием базового случая:
Функция СуммаЧисел(Число)
Возврат Число + СуммаЧисел(Число - 1); // Зацикливание, если Число > 0
КонецФункции
Исправленная версия:
Функция СуммаЧисел(Число)
Если Число = 0 Тогда
Возврат 0; // Базовый случай
Иначе
Возврат Число + СуммаЧисел(Число - 1);
КонецЕсли;
КонецФункции
Всегда проверяйте рекурсивные функции на крайние случаи: пустые входные данные, максимальную глубину вложенности и некорректные параметры.
Рекурсия vs итерация: что выбрать в 1С
Рекурсия — не всегда оптимальное решение. В некоторых случаях итеративные алгоритмы (с использованием циклов) оказываются более эффективными. Давайте сравним оба подхода:
| Критерий | Рекурсия | Итерация |
|---|---|---|
| Читаемость кода | Часто более наглядна для иерархических задач (например, обход деревьев) | Может быть менее интуитивной для вложенных структур |
| Производительность | Менее эффективна из-за накладных расходов на вызов функции | Как правило, быстрее, особенно для больших объёмов данных |
| Память | Потребляет больше памяти (стек вызовов) | Более экономна, так как не требует сохранения контекста |
| Ограничения | Ограничена глубиной стека (~1000 уровней в 1С) | Ограничена только ресурсами системы |
| Типичные задачи | Обход деревьев, рекурсивные алгоритмы (например, быстрая сортировка) | Линейная обработка данных, простые циклы |
Пример задачи, где итерация предпочтительнее: суммирование элементов массива. Рекурсивное решение:
Функция СуммаМассиваРекурсия(Массив, Индекс = 0)
Если Индекс >= Массив.ВГраница() Тогда
Возврат 0;
Иначе
Возврат Массив[Индекс] + СуммаМассиваРекурсия(Массив, Индекс + 1);
КонецЕсли;
КонецФункции
Итеративное решение:
Функция СуммаМассиваИтерация(Массив)
Сумма = 0;
Для Каждого Элемент Из Массив Цикл
Сумма = Сумма + Элемент;
КонецЦикла;
Возврат Сумма;
КонецФункции
В этом случае итерация проще, быстрее и не имеет ограничений по размеру массива. Однако для обхода дерева справочников рекурсия часто оказывается удобнее.
Когда рекурсия опасна в 1С?
При работе с большими объёмами данных (тысячи записей) рекурсия может привести к переполнению стека или значительному замедлению. Например, обход справочника с 5000 групп и 50 000 элементов рекурсивно может занять в десятки раз больше времени, чем итеративно. В таких случаях лучше использовать циклы с явным управлением стеком (например, через Массив или Структура для хранения текущего состояния).
Оптимизация рекурсивных алгоритмов в 1С
Если вы решили использовать рекурсию, её можно оптимизировать следующими способами:
- ⚡ Мемоизация — кэширование результатов вычислений для избежания повторных рекурсивных вызовов с теми же параметрами. Особенно полезно для задач с перекрывающимися подзадачами (например, числа Фибоначчи).
- 🔄 Хвостовая рекурсия — оптимизация, при которой рекурсивный вызов является последней операцией в функции. В некоторых языках это позволяет компилятору преобразовать рекурсию в цикл, но в 1С такая оптимизация не реализована.
- 📉 Ограничение глубины — если глубина рекурсии заранее известна, можно добавить проверку на максимальное количество уровней.
- 🗃️ Использование вспомогательных структур — например, передача аккумулятора (накопленного результата) в параметрах функции для уменьшения количества операций.
Пример мемоизации для функции Фибоначчи:
Перем мКэшФибоначчи;
Функция Фибоначчи(н) Экспорт
Если мКэшФибоначчи = Неопределено Тогда
мКэшФибоначчи = Новый Соответствие;
КонецЕсли;
Если мКэшФибоначчи.СодержитКлюч(н) Тогда
Возврат мКэшФибоначчи[н];
КонецЕсли;
Если н <= 1 Тогда
Возврат н;
Иначе
Результат = Фибоначчи(н - 1) + Фибоначчи(н - 2);
мКэшФибоначчи.Вставить(н, Результат);
Возврат Результат;
КонецЕсли;
КонецФункции
Без мемоизации вычисление Фибоначчи(40) может занять несколько секунд, а с кэшированием — доли секунды.
Базовый случай обработан корректно
Параметры изменяются при каждом вызове
Нет риска переполнения стека (глубина < 1000)
Кэширование применено там, где это уместно
Тестирование на крайние случаи (пустые данные, большие объёмы)-->
Рекурсия в отчётах и обработках 1С
Одно из самых востребованных применений рекурсии в 1С — построение отчётов с иерархической структурой. Например, отчёт по продажам с группировкой по подразделениям, где каждое подразделение может содержать вложенные отделы. Рекурсия позволяет динамически формировать уровни группировки без жёсткого ограничения на количество вложений.
Пример формирования строки отчёта с учётом иерархии:
Процедура ДобавитьСтрокиОтчета(Группа, Уровень = 0)
Выборка = Справочники.Подразделения.Выбрать(,, Группа);
Пока Выборка.Следующий() Цикл
Если Выборка.ЭтоГруппа Тогда
СтрокиОтчета.Добавить();
СтрокиОтчета[СтрокиОтчета.Количество() - 1].Уровень = Уровень;
СтрокиОтчета[СтрокиОтчета.Количество() - 1].Наименование = Повтор(" ", Уровень) + Выборка.Наименование;
ДобавитьСтрокиОтчета(Выборка.Ссылка, Уровень + 1); // Рекурсия для вложенных групп
Иначе
СтрокиОтчета.Добавить();
СтрокиОтчета[СтрокиОтчета.Количество() - 1].Уровень = Уровень;
СтрокиОтчета[СтрокиОтчета.Количество() - 1].Наименование = Повтор(" ", Уровень) + Выборка.Наименование;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
В этом примере параметр Уровень используется для формирования отступов в отчёте, визуально отображающих иерархию. Аналогичный подход применяется при построении деревьев в управляемых формах или печатных формах документов.
Ещё один пример — рекурсивный расчёт итогов по группам номенклатуры. Допустим, нам нужно посчитать суммарный остаток по группе и всем её подгруппам:
Функция РассчитатьОстатокПоГруппе(Группа)
Остаток = 0;
Выборка = РегистрыНакопления.ОстаткиНоменклатуры.ВыбратьОстатки(Группа);
Пока Выборка.Следующий() Цикл
Остаток = Остаток + Выборка.КоличествоОстаток;
КонецЦикла;
// Рекурсия для подгрупп
ВыборкаГрупп = Справочники.Номенклатура.ВыбратьГруппы(Группа);
Пока ВыборкаГрупп.Следующий() Цикл
Остаток = Остаток + РассчитатьОстатокПоГруппе(ВыборкаГрупп.Ссылка);
КонецЦикла;
Возврат Остаток;
КонецФункции
При формировании отчётов с рекурсией всегда учитывайте ограничения платформы 1С на количество строк в таблице значений (по умолчанию — 1 000 000). Для больших отчётов используйте постраничную обработку.
Рекурсия в обмене данными и интеграциях
Рекурсия также полезна при работе с внешними системами, когда данные имеют вложенную структуру. Например, при обмене с 1С:УТ и 1С:БП через УниверсальныйФорматОбменаДанными или при парсинге JSON/XML-ответов от веб-сервисов.
Пример рекурсивного обхода JSON-структуры:
Процедура ОбработатьJSON(Данные, Путь = "")
Если ТипЗнч(Данные) = Тип("Структура") Тогда
Для Каждого Ключ Из Данные Цикл
НовыйПуть = Путь + " -> " + Ключ;
ОбработатьJSON(Данные[Ключ], НовыйПуть); // Рекурсия для вложенных объектов
КонецЦикла;
ИначеЕсли ТипЗнч(Данные) = Тип("Массив") Тогда
Для Инд = 0 По Данные.ВГраница() Цикл
ОбработатьJSON(Данные[Инд], Путь + "[" + Инд + "]");
КонецЦикла;
Иначе
Сообщить(Путь + ": " + Данные);
КонецЕсли;
КонецПроцедуры
Этот код рекурсивно обходит все уровни JSON-объекта, выводя путь к каждому значению. Аналогичный подход применяется при разборе XML-документов с вложенными узлами.
При интеграции с другими системами рекурсия помогает:
- 🔗 Синхронизировать иерархические справочники (например, номенклатуру с категориями из Bitrix24 или MoySklad).
- 📦 Обрабатывать вложенные документы (например, заказы с позициями, которые сами являются комплектами).
- 📡 Формировать сложные запросы к API, где ответ содержит ссылки на другие ресурсы.
При обмене данными с рекурсией всегда предусматривайте обработку циклических ссылок (например, когда элемент справочника ссылается сам на себя через цепочку связей). Это позволит избежать зацикливания.
FAQ: ответы на частые вопросы о рекурсии в 1С
Можно ли в 1С сделать бесконечную рекурсию?
Технически да, но платформа 1С:Предприятие ограничивает глубину стека вызовов (по умолчанию ~1000 уровней). При превышении этого лимита возникает ошибка Переполнение стека. Чтобы увеличить лимит, можно изменить параметр запуска /StackSize в файле 1CEStart.cfg, но это требует прав администратора и не всегда безопасно.
Как отладить рекурсивную функцию в 1С?
Для отладки рекурсии в 1С используйте:
- Пошаговый режим выполнения (F11) с наблюдением за изменением параметров.
- Вывод отладочной информации в окно сообщений (
Сообщить()) на каждом уровне рекурсии. - Проверку тестовыми данными с известным результатом (например, справочник с 2–3 уровнями вложенности).
- Инструмент
Профайлерв конфигураторе для анализа производительности.
Пример отладочного вывода:
Сообщить("Уровень " + Уровень + ", группа: " + Группа.Наименование);
В каких случаях рекурсия в 1С работает медленно?
Рекурсия может тормозить в следующих сценариях:
- 🐢 Глубокая вложенность (тысячи уровней) — каждый вызов функции добавляет накладные расходы.
- 🗄️ Большой объём данных на каждом уровне (например, обход справочника с миллионом элементов).
- 🔄 Повторные вычисления одних и тех же значений (решается мемоизацией).
- 🖥️ Недостаток памяти — рекурсия потребляет больше ресурсов, чем циклы.
В таких случаях замените рекурсию на итеративный алгоритм с явным управлением стеком (например, через Массив или Очередь).
Как обойти ограничение на глубину рекурсии в 1С?
Если вам нужно обработать структуру с глубиной вложенности больше 1000, используйте:
- Итеративный подход — замените рекурсию циклом с собственным стеком (например, через
Массив). - Разбиение задачи — обрабатывайте уровни по частям, сохраняя промежуточные результаты.
- Увеличение стека — измените параметр
/StackSizeв файле запуска (требует прав администратора).
Пример итеративного обхода дерева:
Процедура ОбойтиИтеративно(Корень)
Стек = Новый Массив;
Стек.Добавить(Корень);
Пока Стек.Количество() > 0 Цикл
Текущий = Стек.Удалить(0);
// Обработка текущего узла
Если Текущий.ЭтоГруппа Тогда
Выборка = Справочники.Номенклатура.Выбрать(,, Текущий);
Пока Выборка.Следующий() Цикл
Стек.Добавить(Выборка.Ссылка);
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Можно ли использовать рекурсию в запросах 1С?
Нет, в языке запросов 1С рекурсия напрямую не поддерживается. Однако вы можете:
- Использовать временные таблицы для пошаговой обработки иерархий.
- Применять рекурсивные функции на встроенном языке для постобработки результатов запроса.
- В 1С:Предприятие 8.3.20+ появилась поддержка рекурсивных
ОБЪЕДИНЕНИЕ ВСЕ(рекурсивныеUNION ALL), но синтаксис отличается от стандартного SQL.
Пример рекурсивного запроса (для версий 8.3.20+):
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Подразделения.Ссылка КАК Ссылка,
| Подразделения.Наименование КАК Наименование,
| 0 КАК Уровень
|ПОМЕСТИТЬ ВТ_Подразделения
|ИЗ
| Справочник.Подразделения КАК Подразделения
|ГДЕ
| Подразделения.Родитель = &Корень
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| Подразделения.Ссылка КАК Ссылка,
| Подразделения.Наименование КАК Наименование,
| ВТ_Подразделения.Уровень + 1 КАК Уровень
|ИЗ
| Справочник.Подразделения КАК Подразделения
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Подразделения КАК ВТ_Подразделения
| ПО Подразделения.Родитель = ВТ_Подразделения.Ссылка";