Как перебрать все строки ДеревоЗначений на встроенном языке 1С 8.3
Содержание
Чтобы обойти дерево значений в 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] — вторая в ней. Нумерация с нуля.Как скопировать дерево значений целиком?
Дерево.Скопировать() возвращает новое дерево с той же структурой и данными. Изменения копии не затрагивают исходник.Как отсортировать дерево с учётом вложенных уровней?
Дерево.Строки.Сортировать("Сумма Убыв", Истина). Второй параметр включает сортировку подчинённых строк на всех уровнях.Можно ли получить из дерева таблицу значений?
ЗаполнитьЗначенияСвойств. Чаще всего переносят только листья.