ДеревоЗначений в 1С 8.3: чтение строк, обход веток и поиск узлов на встроенном языке

· 9 мин

Содержание

Чтобы получить значение из дерева значений в 1С, сначала доберитесь до нужной строки, а потом прочитайте колонку по имени: Строка.Сумма или Строка["Сумма"]. Строки верхнего уровня лежат в Дерево.Строки, подчинённые — в Строка.Строки у каждого узла. Поэтому всё «получение значения» сводится к двум задачам: найти узел и правильно обратиться к его ячейке.

На управляемой форме синтаксис другой. Там дерево хранится как ДанныеФормыДерево, вместо свойства Строки используется метод ПолучитьЭлементы(), а текущую строку дают Элементы.ИмяДерева.ТекущиеДанные. Ниже — оба варианта, рабочие примеры кода и ошибки, на которых чаще всего спотыкаются.

Как устроен объект ДеревоЗначений

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

Объект состоит из трёх частей:

  • 🧱 Колонки — коллекция колонок. Одна на всё дерево: у вложенных строк те же колонки, что и у верхних.
  • 🌳 Строки — коллекция строк верхнего уровня (КоллекцияСтрокДереваЗначений).
  • 📄 СтрокаДереваЗначений — отдельный узел. В нём лежат значения колонок, ссылка на родителя и своя коллекция Строки.
ОбъектЧто даётКлючевые свойства и методы
ДеревоЗначенийвсё дерево целикомКолонки, Строки, Скопировать()
КоллекцияСтрокДереваЗначенийстроки одного уровня у одного родителяДобавить(), Количество(), Найти(), НайтиСтроки(), Итог(), Сортировать(), ВыгрузитьКолонку(), Родитель
СтрокаДереваЗначенийодин узелРодитель, Строки, Уровень(), доступ к колонкам по имени и индексу

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

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

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

Товар = Группа.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Количество = 10;
Товар.Сумма = 3500;

Товар = Группа.Строки.Добавить();
Товар.Наименование = "Ручка шариковая";
Товар.Количество = 50;
Товар.Сумма = 1250;

Типизируйте колонки через ОписаниеТипов. Колонка без типа принимает что угодно, и ошибку «строка вместо числа» вы увидите только на этапе сложения или вывода в отчёт.

Доступ к строкам: индекс, перебор, текущая строка

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

ПерваяГруппа = Дерево.Строки[0];
ВторойТовар = ПерваяГруппа.Строки[1];
Сообщить(ВторойТовар.Наименование); // Ручка шариковая

Перед обращением по индексу проверяйте размер коллекции. Дерево.Строки[0] на пустом дереве вызовет ошибку Индекс находится за границами диапазона.

Если Дерево.Строки.Количество() > 0 Тогда
    ПерваяГруппа = Дерево.Строки[0];
КонецЕсли;

Перебор одного уровня — обычный цикл Для Каждого. Он не заходит во вложенные строки, только в строки текущей коллекции:

Для Каждого Группа Из Дерево.Строки Цикл
    Сообщить(Группа.Наименование + ": " + Группа.Строки.Количество() + " поз.");
КонецЦикла;

Номер строки внутри своей коллекции возвращает метод Индекс(): Группа.Строки.Индекс(Товар). Если строка не принадлежит этой коллекции, вернётся -1.

Родитель, подчинённые строки и уровень вложенности

Иерархию читают тремя инструментами:

  1. Строка.Родитель — узел уровнем выше. У строк верхнего уровня он равен Неопределено.
  2. Строка.Строки — дочерние узлы. Пустая коллекция означает «лист», последний уровень.
  3. Строка.Уровень() — глубина узла. Верхний уровень — 0, его дети — 1 и так далее.

Так можно подняться от любого узла к корню и собрать полный путь:

Функция ПутьКУзлу(Строка)
    Путь = Строка.Наименование;
    Текущая = Строка.Родитель;
    Пока Текущая <> Неопределено Цикл
        Путь = Текущая.Наименование + " / " + Путь;
        Текущая = Текущая.Родитель;
    КонецЦикла;
    Возврат Путь;
КонецФункции

Для обхода всех веток сверху вниз нужна рекурсия. Процедура принимает коллекцию строк и вызывает сама себя для каждой коллекции детей:

Процедура ОбойтиДерево(КоллекцияСтрок)
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Отступ = СтрокаСимволов("  ", Строка.Уровень());  // своя функция, см. ниже
        Сообщить(Отступ + Строка.Наименование);
        Если Строка.Строки.Количество() > 0 Тогда
            ОбойтиДерево(Строка.Строки);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

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

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

Отличить группу от листа по пустой колонке ненадёжно: у группы тоже может быть заполнено «Количество». Проверяйте Строка.Строки.Количество() > 0 или храните признак в отдельной колонке ЭтоГруппа.

Чтение ячеек и работа с колонками

У строки дерева три равноценных способа прочитать значение:

СпособПримерКогда удобен
Через точкуТовар.Суммаимя колонки известно при написании кода
По имени в скобкахТовар["Сумма"]имя приходит из переменной или настроек
По индексу колонкиТовар[2]универсальный перебор всех колонок

Перебор всех ячеек строки, не зная заранее, какие колонки есть:

Для Каждого Колонка Из Дерево.Колонки Цикл
    Значение = Товар[Колонка.Имя];
    Сообщить(Колонка.Имя + " = " + Строка(Значение));
КонецЦикла;

Проверить, есть ли колонка, можно до обращения к ней: Дерево.Колонки.Найти("Сумма") вернёт колонку или Неопределено. Это спасает от ошибки, когда дерево собирается в одном месте кода, а читается в другом.

Чтобы получить все значения одной колонки уровня сразу, используйте ВыгрузитьКолонку() — он вернёт массив:

Названия = Группа.Строки.ВыгрузитьКолонку("Наименование");

Сумму по колонке считает Итог(). Второй параметр включает подчинённые строки:

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

Итог с подчинёнными строками суммирует и группы, и вложенные товары. Если вы уже записали в строку группы сумму её товаров, Итог("Сумма", Истина) посчитает каждую сумму дважды. В таком дереве берите итог только по верхнему уровню или только по листьям.

Поиск узла: Найти и НайтиСтроки

Метод Найти(Значение, Колонки, ВключатьПодчиненные) возвращает первую подходящую строку или Неопределено. По умолчанию он ищет только в той коллекции, у которой вызван.

// Ищем во всём дереве, в колонке "Наименование"
Узел = Дерево.Строки.Найти("Бумага А4", "Наименование", Истина);
Если Узел <> Неопределено Тогда
    Сообщить("Сумма: " + Узел.Сумма);
КонецЕсли;

НайтиСтроки(Отбор, ВключатьПодчиненные) возвращает массив всех совпадений. Отбор — структура «колонка → значение», условия объединяются через «И»:

Отбор = Новый Структура("Количество", 10);
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из Найденные Цикл
    Сообщить(ПутьКУзлу(Строка) + " — " + Строка.Сумма);
КонецЦикла;
МетодЧто вернётУсловиеГлубина
Найти()одну строку или Неопределеноодно значение в одной или нескольких колонкахпо третьему параметру
НайтиСтроки()массив строк (может быть пустым)точное равенство по всем полям структурыпо второму параметру
Своя рекурсиячто угоднолюбые условия: «больше», «содержит», по типувся иерархия

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

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

На форме дерево — это реквизит типа ДеревоЗначений, но в модуле формы на клиенте вы работаете с объектом ДанныеФормыДерево. Отличия от «серверного» дерева:

  • 🔁 вместо свойства Строки — метод ПолучитьЭлементы();
  • ⬆️ вместо свойства Родитель — метод ПолучитьРодителя();
  • 🆔 у строки есть идентификатор ПолучитьИдентификатор(), по нему строку находит НайтиПоИдентификатору();
  • 🚫 методов Найти(), НайтиСтроки() и Итог() у коллекции элементов формы нет.

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

&НаКлиенте
Процедура ДеревоТоваровВыбор(Элемент, ВыбраннаяСтрока, Поле, СтандартнаяОбработка)
    ТекДанные = Элементы.ДеревоТоваров.ТекущиеДанные;
    Если ТекДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    Сообщить(ТекДанные.Наименование + ": " + ТекДанные.Сумма);
    Родитель = ТекДанные.ПолучитьРодителя();
    Если Родитель <> Неопределено Тогда
        Сообщить("Группа: " + Родитель.Наименование);
    КонецЕсли;
КонецПроцедуры

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

Если нужен полноценный поиск или итоги, переведите реквизит в обычное дерево на сервере и обратно:

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

Развернуть ветки на форме можно методом элемента: Элементы.ДеревоТоваров.Развернуть(Идентификатор, Истина). Второй параметр раскрывает и всех потомков. Для раскрытия всего дерева пройдите по ПолучитьЭлементы() верхнего уровня и вызовите Развернуть для каждой строки.

Дерево из запроса и большие объёмы данных

Чаще всего дерево не собирают руками, а получают из запроса с итогами. Ключевое слово ИТОГИ ПО плюс обход по группировкам дают готовую иерархию:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Номенклатура.Родитель КАК Группа,
|    Номенклатура.Ссылка КАК Номенклатура
|ИЗ
|    Справочник.Номенклатура КАК Номенклатура
|ГДЕ
|    НЕ Номенклатура.ЭтоГруппа
|ИТОГИ ПО
|    Группа";

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

Если итоги считать не по иерархии справочника, а по полю ИЕРАРХИЯ (ИТОГИ ПО Номенклатура ИЕРАРХИЯ), дерево повторит структуру групп справочника на всю глубину.

Когда в дереве десятки тысяч строк, скорость решают несколько приёмов:

  • ⚡ Соответствие вместо повторного поиска. У дерева, в отличие от таблицы значений, нет индексов. Каждый Найти(..., Истина) проходит всё дерево заново. Если ищете в цикле, один раз соберите Соответствие «ключ → строка дерева» и берите узлы из него.
  • 🧮 Итоги в запросе, а не в коде. ИТОГИ СУММА(...) ПО считает быстрее, чем Итог() по каждой ветке.
  • 📦 Выборка вместо выгрузки. Если дерево нужно только прочитать один раз, Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) с вложенными Выбрать() не держит в памяти весь результат.
  • 🔄 Меньше передач клиент-сервер. РеквизитФормыВЗначение и ЗначениеВРеквизитФормы копируют всё дерево. Делайте их один раз за серверный вызов, а не в цикле.

Пример быстрого доступа по ключу:

Карта = Новый Соответствие;
ЗаполнитьКарту(Дерево.Строки, Карта);

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

// Дальше в цикле по документам — без поиска по дереву:
Узел = Карта.Получить(ТоварДокумента);

В модуле процедура ЗаполнитьКарту объявляется до основного кода или в общем модуле. Строки дерева в Соответствии — ссылки на те же объекты: изменение Узел.Сумма сразу меняет дерево.

Ошибки при работе с деревом значений

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

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

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

Ещё одна ловушка — Сортировать(). Без второго параметра Истина сортируется только тот уровень, у которого вызван метод: Дерево.Строки.Сортировать("Сумма Убыв", Истина) упорядочит все ветки.

Чем дерево значений отличается от таблицы значений?
У таблицы строки плоские, у дерева каждая строка может иметь подчинённые. Колонки общие для всех уровней. Индексов для ускорения поиска у дерева нет, у таблицы есть.
Можно ли передать ДеревоЗначений на клиент?
Напрямую нет — обычное дерево значений существует только на сервере. На форме его заменяет реквизит, который на клиенте виден как ДанныеФормыДерево.
Как превратить дерево в таблицу значений?
Создайте таблицу с теми же колонками и рекурсивно добавьте в неё каждую строку дерева, копируя значения через ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева).
Как скопировать дерево, чтобы правки не затронули исходное?
Метод Скопировать() создаёт новое дерево с теми же колонками и строками. Изменения в копии на оригинал не влияют.
Как узнать, что строка — последний уровень?
Проверьте Строка.Строки.Количество() = 0. Для дерева из запроса с итогами надёжнее смотреть, заполнено ли поле детальной записи.