Как правильно реализовать поиск значения в иерархии справочника 1С: рекурсия против цикла

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом IT и автоматизация бизнеса
← На главную

При разработке на платформе 1С:Предприятие часто возникает задача получения данных не только из текущего элемента справочника, но и из его вышестоящих уровней (родителей). Например, в конфигурации 1С:Зарплата и управление персоналом может потребоваться найти настройки учета, которые задаются для подразделения, а если они не заполнены — «подтянуть» их из головного подразделения. В этой статье мы разберем, как правильно реализовать такой поиск, почему классическая рекурсия часто работает некорректно и как написать максимально эффективный код.

Анализ типичной ошибки в рекурсивной функции

Рассмотрим ситуацию, с которой столкнулся автор темы. Перед ним стояла задача: если числовое поле Тест у элемента заполнено, вернуть его значение. Если нет — проверить родителя, и так далее до самого верха иерархии. Автор попытался использовать рекурсию, но столкнулся с проблемой: на определенном этапе функция переставала возвращать результат.

Проанализируем исходный код:


&НаСервере
Функция ВернутьДни(Знач ВыбПодразделение) 
    Если ВыбПодразделение.Родитель.Пустая() Тогда  
        Возврат ВыбПодразделение.Тест;
    Иначе
        Если ВыбПодразделение.Тест = 0 Тогда  
            ВернутьДни(ВыбПодразделение.Родитель); // Ошибка здесь!
        Иначе 
            Возврат ВыбПодразделение.Тест;
        КонецЕсли;    
    КонецЕсли;    
КонецФункции

В чем заключается проблема? В 1С, как и во многих других языках программирования, если функция должна вернуть значение, то каждый путь выполнения кода должен заканчиваться оператором Возврат. В строке, где вызывается ВернутьДни(ВыбПодразделение.Родитель), происходит вызов функции, она отрабатывает, находит значение глубоко в иерархии, возвращает его в точку вызова... и это значение просто «повисает в воздухе», так как текущий экземпляр функции ничего не передает выше по стеку.

Правильное решение для рекурсии: необходимо добавить ключевое слово Возврат перед самим рекурсивным вызовом:

Возврат ВернутьДни(ВыбПодразделение.Родитель);

Тогда результат, полученный от «родительского» вызова, будет последовательно передан по всей цепочке обратно в самую первую вызванную функцию.

Почему рекурсия — не лучший выбор для этой задачи?

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

  1. Переполнение стека (Stack Overflow): Каждая итерация рекурсии потребляет память для хранения контекста функции. Если иерархия справочника окажется аномально глубокой (сотни или тысячи уровней), программа может аварийно завершиться.
  2. Производительность: Рекурсия в 1С работает несколько медленнее, чем обычный цикл.
  3. Читаемость: Линейный код проще отлаживать. При поиске ошибки в рекурсии приходится «прыгать» по уровням стека в конфигураторе, что затрудняет понимание текущего состояния переменных.

В данном случае мы имеем дело с классическим подъемом по дереву, который идеально ложится в итерационную логику.

Реализация через итерационный цикл (Пока)

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


&НаСервере
Функция ПолучитьДниИерархически(Знач ТекущееПодразделение) Экспорт
    
    Результат = 0;
    
    // Цикл будет работать, пока ссылка на подразделение не станет пустой
    Пока ЗначениеЗаполнено(ТекущееПодразделение) Цикл
        
        // Получаем значение из текущего элемента
        ЗначениеТест = ТекущееПодразделение.Тест;
        
        Если ЗначениеТест <> 0 Тогда
            Результат = ЗначениеТест;
            Прервать; // Значение найдено, выходим из цикла
        КонецЕсли;
        
        // Переходим к родителю для следующей итерации
        ТекущееПодразделение = ТекущееПодразделение.Родитель;
        
    КонецЦикла;
    
    Возврат Результат;
    
КонецФункции

Этот код более надежен и эффективен. Мы просто «шагаем» вверх по иерархии. Если на каком-то уровне поле Тест заполнено, мы фиксируем результат и прерываем цикл. Если мы дошли до элемента, у которого нет родителя (корень), цикл завершится сам собой.

Важные аспекты оптимизации производительности

Рассмотрим ситуацию глубже. В 1С обращение через точку, такое как ТекущееПодразделение.Родитель или ТекущееПодразделение.Тест, в коде модуля может приводить к неявным запросам к базе данных. Если мы вызываем нашу функцию в цикле (например, при формировании отчета по тысяче сотрудников), количество запросов к БД станет огромным. Это классическая проблема «N+1 запрос».

Для оптимизации профессионалы используют следующие приемы:

1. Использование механизма «Повторное использование значений»

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

2. Запрос вместо объектной модели

Если нужно получить данные один раз для одного элемента, объектный обход допустим. Но если нужно обработать массив данных, лучше использовать один запрос с конструкцией В ИЕРАРХИИ или предварительно выбрать всю цепочку родителей в таблицу значений.

Проанализируем пример запроса, который получает всех родителей для конкретного узла:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    Подразделения.Тест КАК Тест,
    |    Подразделения.Уровень КАК Уровень
    |ИЗ
    |    Справочник.ПодразделенияОрганизаций КАК Подразделения
    |ГДЕ
    |    Подразделения.Ссылка В ИЕРАРХИИ(&ВыбПодразделение)
    |УПОРЯДОЧИТЬ ПО
    |    Уровень УБЫВ";

Такой запрос вернет все подразделения «выше» текущего. Нам останется только перебрать их в цикле и взять первое попавшееся ненулевое значение. Это гораздо быстрее, чем делать 10–20 мелких запросов к БД при обращении через точку в коде.

Заключение

Подведем итоги нашего разбора:

  1. В рекурсии всегда используйте Возврат при вызове функции саму себя, чтобы результат передавался вверх по стеку.
  2. Предпочитайте циклы рекурсии при линейном обходе иерархии (от ребенка к родителю) — это безопаснее и быстрее.
  3. Помните о производительности: избегайте множественных обращений через точку в циклах. Используйте кэширование (повторное использование значений) или SQL-запросы для массовой обработки данных.
  4. Именуйте функции правильно: префикс Знач в параметрах гарантирует, что вы не измените исходную ссылку в процессе работы алгоритма, что критически важно для чистоты кода.

Используя эти правила, вы сможете создавать надежные и быстрые механизмы обработки иерархических данных в любой конфигурации 1С.

← На главную