Как перебрать все строки ДеревоЗначений на встроенном языке 1С 8.3

· 8 мин

Содержание

Чтобы обойти дерево значений в 1С целиком, перебирают Дерево.Строки, а для каждой строки рекурсивно вызывают ту же процедуру для её коллекции Строка.Строки. Цикл Для Каждого по Дерево.Строки видит только верхний уровень — вложенные строки он пропустит.

Типичная ситуация: запрос с ИТОГИ ПО выгружен в дерево, нужно пройти по всем контрагентам внутри групп и пересчитать сумму. Ниже — рабочие приёмы для 1С:Предприятия 8.3: рекурсия, обход через стек, поиск, итоги, правка структуры и особенности формы.

Как устроено дерево значений: строки, колонки, уровни

Объект ДеревоЗначений состоит из двух частей:

  • 🧱 Колонки — общий для всех уровней набор колонок. Отдельных колонок для групп и элементов нет.
  • 🌳 Строки — коллекция строк верхнего уровня (КоллекцияСтрокДереваЗначений).

У каждой строки (СтрокаДереваЗначений) есть:

Свойство / методЧто возвращает
Строка.СтрокиКоллекция подчинённых строк (может быть пустой)
Строка.РодительСтрока-родитель; у верхнего уровня — Неопределено
Строка.Уровень()Номер уровня, верхний — 0
Строка.ИмяКолонкиЗначение колонки

Иерархия хранится только в связях «родитель — подчинённые». Глобального списка всех строк у дерева нет. Поэтому любой полный перебор — это спуск по коллекциям Строки.

Тестовое дерево для примеров:

Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));

Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Покупатели";

Стр = Группа.Строки.Добавить();
Стр.Наименование = "ООО Альфа";
Стр.Сумма = 1500;

Стр = Группа.Строки.Добавить();
Стр.Наименование = "ООО Бета";
Стр.Сумма = 2500;

Задавайте колонкам тип через ОписаниеТипов. Числовая колонка без типа хранит что угодно, и метод Итог() на ней ведёт себя непредсказуемо.

Рекурсивный обход дерева значений с сохранением иерархии

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

Процедура ОбойтиСтроки(КоллекцияСтрок)

    Для Каждого Строка Из КоллекцияСтрок Цикл

        Отступ = СтрокаСимволов(" ", Строка.Уровень() * 4);
        Сообщить(Отступ + Строка.Наименование + ": "
            + Формат(Строка.Сумма, "ЧН=0; ЧГ=0"));

        ОбойтиСтроки(Строка.Строки);

    КонецЦикла;

КонецПроцедуры

// Вызов
ОбойтиСтроки(Дерево.Строки);

Порядок вывода — «сверху вниз, сначала в глубину»: группа, затем все её элементы, затем следующая группа. Это удобно для печати и выгрузки, где важна вложенность.

Функции СтрокаСимволов в платформе нет, её нужно написать или заменить циклом:

Функция СтрокаСимволов(Символ, Количество)
    Результат = "";
    Для Сч = 1 По Количество Цикл
        Результат = Результат + Символ;
    КонецЦикла;
    Возврат Результат;
КонецФункции

Когда нужна обработка «снизу вверх» (сначала дети, потом родитель), переставьте строки: рекурсивный вызов — до обработки текущей строки. Этот вариант понадобится для итогов.

Перебор всех строк дерева без рекурсии

Иногда рекурсия неудобна: код в общем модуле с ограничением на вспомогательные процедуры, очень глубокое дерево или нужна плоская выборка. Тогда обход делают через массив-стек.

Стек = Новый Массив;
Для Каждого Строка Из Дерево.Строки Цикл
    Стек.Добавить(Строка);
КонецЦикла;

Пока Стек.Количество() > 0 Цикл

    ПоследнийИндекс = Стек.ВГраница();
    Строка = Стек[ПоследнийИндекс];
    Стек.Удалить(ПоследнийИндекс);

    // обработка строки
    Если Строка.Строки.Количество() = 0 Тогда
        Сообщить(Строка.Наименование);
    КонецЕсли;

    Для Каждого Подчиненная Из Строка.Строки Цикл
        Стек.Добавить(Подчиненная);
    КонецЦикла;

КонецЦикла;

Стек отдаёт строки в обратном порядке внутри уровня. Если порядок важен, кладите подчинённые с конца: Для Сч = 1 По Строка.Строки.Количество() Цикл и Строка.Строки[Строка.Строки.Количество() - Сч].

Для обхода «по уровням» (все строки уровня 0, потом уровня 1) возьмите вместо стека очередь: добавляйте в конец массива, забирайте элемент с индексом 0.

Плоский список всех строк без цикла можно получить и одной строкой:

ВсеСтроки = Дерево.Строки.НайтиСтроки(Новый Структура, Истина);

Пустая структура отбора подходит под любую строку, а второй параметр Истина включает подчинённые уровни. Результат — Массив строк дерева, по нему обычный Для Каждого.

Поиск строки в дереве значений

Платформа ищет быстрее, чем любой цикл на встроенном языке. Два метода коллекции строк:

МетодЧто вернётКогда брать
Найти(Значение, Колонки, ВключатьПодчиненные)Первую строку или НеопределеноНужна одна строка по значению
НайтиСтроки(Отбор, ВключатьПодчиненные)Массив строк (может быть пустым)Отбор по нескольким колонкам, все совпадения
// Одна строка по наименованию на всех уровнях
Найденная = Дерево.Строки.Найти("ООО Бета", "Наименование", Истина);
Если Найденная <> Неопределено Тогда
    Сообщить("Родитель: " + Найденная.Родитель.Наименование);
КонецЕсли;

// Все строки с нулевой суммой
Отбор = Новый Структура("Сумма", 0);
Пустые = Дерево.Строки.НайтиСтроки(Отбор, Истина);

Без третьего параметра Истина поиск идёт только по коллекции, у которой вызван метод. Это частая причина «не находит, хотя строка есть»: строка лежит на втором уровне.

Поиск можно сузить веткой: Группа.Строки.Найти(...) смотрит только внутри группы.

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

Функция НайтиПоУсловию(КоллекцияСтрок, Порог)
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Если Строка.Сумма > Порог Тогда
            Возврат Строка;
        КонецЕсли;
        Результат = НайтиПоУсловию(Строка.Строки, Порог);
        Если Результат <> Неопределено Тогда
            Возврат Результат;
        КонецЕсли;
    КонецЦикла;
    Возврат Неопределено;
КонецФункции

Чтобы восстановить путь к найденной строке, поднимайтесь по Родитель, пока не получите Неопределено.

Итоги по узлам дерева значений

Метод Итог(Колонка, ВключатьВсе) считает сумму числовой колонки по коллекции строк:

ПоГруппе = Группа.Строки.Итог("Сумма");        // только прямые подчинённые
ВсёДерево = Дерево.Строки.Итог("Сумма", Истина); // все уровни

Подвох: при Истина в сумму входят и строки групп. Если в группах уже стоит итог, он сложится с элементами второй раз. Для данных из запроса с ИТОГИ ПО так и происходит — группировки уже содержат суммы.

Надёжнее заполнить итоги групп самостоятельно, обходом снизу вверх:

Функция ПосчитатьИтоги(КоллекцияСтрок)

    Сумма = 0;
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Если Строка.Строки.Количество() > 0 Тогда
            Строка.Сумма = ПосчитатьИтоги(Строка.Строки);
        КонецЕсли;
        Сумма = Сумма + Строка.Сумма;
    КонецЦикла;
    Возврат Сумма;

КонецФункции

Общая = ПосчитатьИтоги(Дерево.Строки);

Проверка на тестовом дереве: у «ООО Альфа» 1 500, у «ООО Бета» 2 500. Функция запишет в группу «Покупатели» 4 000 и вернёт 4 000 как общий итог. Повторный запуск даст тот же результат: значения групп перезаписываются, а не накапливаются.

Тем же каркасом считают количество элементов, максимум, долю строки в группе. Меняется только строка с накоплением.

Добавление, удаление и перенос строк

Новую строку добавляют в нужную коллекцию:

  • ➕ Дерево.Строки.Добавить() — на верхний уровень;
  • ➕ Группа.Строки.Добавить() — в группу;
  • ➕ Группа.Строки.Вставить(0) — в начало группы.

Удаление — через коллекцию, которой строка принадлежит. Для верхнего уровня это Дерево.Строки, для остальных — Строка.Родитель.Строки:

Процедура УдалитьСтрокуДерева(Дерево, Строка)
    Если Строка.Родитель = Неопределено Тогда
        Дерево.Строки.Удалить(Строка);
    Иначе
        Строка.Родитель.Строки.Удалить(Строка);
    КонецЕсли;
КонецПроцедуры

Строка удаляется вместе со всеми подчинёнными.

Не удаляйте строки внутри Для Каждого по той же коллекции. После удаления коллекция сдвигается, и цикл пропускает следующую строку.

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

Процедура УдалитьНулевые(КоллекцияСтрок)
    Количество = КоллекцияСтрок.Количество();
    Для Сч = 1 По Количество Цикл
        Строка = КоллекцияСтрок[Количество - Сч];
        УдалитьНулевые(Строка.Строки);
        Если Строка.Сумма = 0 И Строка.Строки.Количество() = 0 Тогда
            КоллекцияСтрок.Удалить(Строка);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

Индекс считается от исходного количества, которое запомнили до цикла. Удаление идёт с конца, поэтому ещё не пройденные строки не сдвигаются.

Метода «сменить родителя» у строки нет. Сдвинуть() меняет позицию только внутри своей коллекции. Чтобы перенести ветку в другую группу, её копируют и удаляют оригинал:

Процедура СкопироватьВетку(Источник, КоллекцияПриемник)
    Новая = КоллекцияПриемник.Добавить();
    ЗаполнитьЗначенияСвойств(Новая, Источник);
    Для Каждого Подчиненная Из Источник.Строки Цикл
        СкопироватьВетку(Подчиненная, Новая.Строки);
    КонецЦикла;
КонецПроцедуры

СкопироватьВетку(СтрокаДляПереноса, НоваяГруппа.Строки);
УдалитьСтрокуДерева(Дерево, СтрокаДляПереноса);

Ветку нельзя переносить внутрь самой себя: копирование уйдёт в бесконечный цикл. Перед переносом проверьте, что новая группа не лежит среди потомков переносимой строки (поднимитесь от неё по Родитель).

Дерево значений из запроса и на управляемой форме

Дерево чаще всего получают из запроса с итогами:

Результат = Запрос.Выполнить();
Дерево = Результат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Если дерево нужно только для чтения, его можно не создавать. Выборка Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) обходится вложенными циклами Пока Выборка.Следующий() с вызовом Выбрать() на каждом уровне. Это экономит память на больших отчётах.

На управляемой форме реквизит типа «Дерево значений» живёт как ДанныеФормыДерево. Его методы другие:

Объект в модулеПодчинённые строкиРодитель
ДеревоЗначений (сервер)Строка.СтрокиСтрока.Родитель
ДанныеФормыДерево (клиент и сервер)Элемент.ПолучитьЭлементы()Элемент.ПолучитьРодителя()

Рекурсивный обход на клиенте выглядит так:

&НаКлиенте
Процедура ОбойтиЭлементы(Элементы)
    Для Каждого Элемент Из Элементы Цикл
        // обработка Элемент.Сумма
        ОбойтиЭлементы(Элемент.ПолучитьЭлементы());
    КонецЦикла;
КонецПроцедуры

// Вызов
ОбойтиЭлементы(Объект.Дерево.ПолучитьЭлементы());

Методов Найти и Итог у данных формы нет. Для тяжёлой обработки переведите реквизит в объект на сервере: РеквизитФормыВЗначение("Дерево"), обработайте, затем верните ЗначениеВРеквизитФормы(Дерево, "Дерево").

Текущую строку дерева на форме получают через Элементы.Дерево.ТекущаяСтрока (это идентификатор) и Объект.Дерево.НайтиПоИдентификатору(...).

Ошибки при обходе и скорость работы

Диагностика: выберите симптом

Что влияет на скорость:

  • ⚡ Найти и НайтиСтроки работают внутри платформы и обгоняют цикл на встроенном языке. Индексов, как у таблицы значений, у дерева нет, поэтому каждый вызов всё равно просматривает строки.
  • ⚡ Не вызывайте Найти с Истина внутри обхода всего дерева: получится квадратичная сложность. Соберите нужные строки в Соответствие за один проход, потом ищите в нём по ключу.
  • ⚡ Передача дерева между клиентом и сервером дорогая. Обрабатывайте его на сервере одним вызовом, а не строкой за раз.
  • ⚡ Если иерархия не нужна, выгрузите запрос в таблицу значений: она легче, у неё есть индексы.

Частые вопросы

Как узнать, что строка дерева — группа, а не элемент?
Проверьте Строка.Строки.Количество() > 0. Признака «группа» у строки дерева нет, есть только наличие подчинённых.
Как получить строку дерева по номеру?
Через индекс коллекции: Дерево.Строки[0] — первая строка верхнего уровня, Дерево.Строки[0].Строки[1] — вторая в ней. Нумерация с нуля.
Как скопировать дерево значений целиком?
Метод Дерево.Скопировать() возвращает новое дерево с той же структурой и данными. Изменения копии не затрагивают исходник.
Как отсортировать дерево с учётом вложенных уровней?
Вызовите Дерево.Строки.Сортировать("Сумма Убыв", Истина). Второй параметр включает сортировку подчинённых строк на всех уровнях.
Можно ли получить из дерева таблицу значений?
Готового метода нет. Создайте таблицу с теми же колонками и обойдите дерево, добавляя строки через ЗаполнитьЗначенияСвойств. Чаще всего переносят только листья.