Как перенести дерево значений в другое дерево и не испортить исходник
Содержание
Чтобы скопировать дерево значений в 1С целиком, вызовите ДеревоКопия = Дерево.Скопировать(); — получится новое независимое дерево с теми же колонками и всей иерархией строк. Если нужно перенести строки в уже существующее дерево, под конкретный узел или с отбором, пишут рекурсивную процедуру: на каждом уровне добавляют строку через Строки.Добавить(), заполняют её ЗаполнитьЗначенияСвойств() и спускаются в подчинённые строки.
Типичная ситуация: разработчик строит в отчёте или обработке дерево «Контрагент → Договор → Документ», хочет сохранить «снимок» до пересчёта, пишет Снимок = Дерево; — и через минуту видит, что снимок пересчитался вместе с оригиналом. Ниже разобраны все рабочие способы копирования для платформы 8.3: встроенный метод, рекурсия, обход без рекурсии, копирование со сменой структуры и работа с деревьями на управляемой форме.
Почему присваивание не создаёт копию дерева
ДеревоЗначений — объект, а переменная хранит ссылку на него. Строка Дерево2 = Дерево1; не копирует данные, а даёт второе имя тому же объекту в памяти.
Дерево2 = Дерево1;
Дерево2.Строки[0].Сумма = 0;
// Дерево1.Строки[0].Сумма теперь тоже 0 — это один и тот же объект
То же происходит при передаче дерева в процедуру как параметра: внутри процедуры вы меняете оригинал. Ключевое слово Знач перед параметром не спасает — для объектов оно защищает только саму переменную от переприсваивания, но не содержимое дерева.
Правило простое: всё, что создаётся через Новый (дерево, таблица, массив, структура), при присваивании передаётся по ссылке. Копию всегда нужно делать явно.
Метод Скопировать() для дерева значений и его ограничения
Самый короткий и самый быстрый путь — встроенный метод. Он выполняется внутри платформы, без построчного цикла на встроенном языке.
ДеревоКопия = ДеревоИсходное.Скопировать();
ДеревоКопия.Строки[0].Сумма = 0; // исходное дерево не изменилось
Что делает метод:
- ✅ создаёт новый объект
ДеревоЗначений; - ✅ переносит все колонки с типами, заголовками и шириной;
- ✅ копирует все строки на всех уровнях с сохранением порядка и иерархии.
Где его возможностей не хватает:
- ❌ нельзя скопировать в существующее дерево. Метод всегда возвращает новый объект. Если приёмник уже заполнен или привязан к чему-то (например, передан в другую процедуру),
Скопировать()не подходит; - ❌ нельзя выбрать часть строк или колонок. В отличие от
ТаблицаЗначений.Скопировать(Строки, Колонки), у дерева метод копирует всё; - ❌ нельзя скопировать одну ветку. У строки дерева (
СтрокаДереваЗначений) метода копирования нет; - ❌ вложенные объекты в ячейках не дублируются. Если в колонке лежит массив, структура или таблица значений, копия получит ссылку на тот же объект. Меняете массив в копии — меняется и в исходнике.
Ссылки на справочники и документы (СправочникСсылка, ДокументСсылка) копировать «глубоко» не нужно: ссылка неизменяема, её копия ничем не хуже оригинала. Проблема возникает только с изменяемыми коллекциями внутри ячеек.
Пустое дерево с той же структурой колонок
Частая подзадача — получить приёмник с такими же колонками, но без строк. Удобный приём:
Приемник = Источник.Скопировать();
Приемник.Строки.Очистить();
На больших деревьях это расточительно: сначала копируются все строки, потом удаляются. Тогда колонки переносят циклом:
Функция НовоеДеревоПоОбразцу(Источник)
Приемник = Новый ДеревоЗначений;
Для Каждого Колонка Из Источник.Колонки Цикл
Приемник.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения, Колонка.Заголовок, Колонка.Ширина);
КонецЦикла;
Возврат Приемник;
КонецФункции
Рекурсивный обход: копируем строки в другое дерево вручную
Когда встроенного метода мало, строки переносят сами. Структура дерева рекурсивна: у дерева есть коллекция Строки, у каждой строки — своя коллекция Строки с подчинёнными. Поэтому одна процедура, вызывающая сама себя, обходит дерево любой глубины.
Алгоритм на каждом уровне:
- Перебрать строки источника текущего уровня.
- Для каждой добавить строку в коллекцию приёмника:
СтрокиПриемник.Добавить(). - Заполнить её значения из строки источника:
ЗаполнитьЗначенияСвойств(). - Вызвать ту же процедуру для подчинённых строк обеих сторон.
ЗаполнитьЗначенияСвойств(Приемник, Источник) переносит значения колонок с совпадающими именами. Колонки, которых нет в приёмнике, пропускаются без ошибки. Это и делает ручное копирование гибким: приёмник может иметь другой набор колонок.
Пример кода рекурсивного копирования дерева значений
Базовая процедура. Она работает в любом контексте, где доступен тип ДеревоЗначений: на сервере, в толстом клиенте, во внешнем соединении.
// Копирует все строки СтрокиИсточник (со всеми уровнями) в коллекцию СтрокиПриемник.
Процедура СкопироватьСтрокиДерева(СтрокиИсточник, СтрокиПриемник) Экспорт
Для Каждого СтрокаИсточник Из СтрокиИсточник Цикл
НоваяСтрока = СтрокиПриемник.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
Если СтрокаИсточник.Строки.Количество() > 0 Тогда
СкопироватьСтрокиДерева(СтрокаИсточник.Строки, НоваяСтрока.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Варианты вызова под разные задачи:
// 1. Всё дерево целиком в новое дерево с такими же колонками
Приемник = НовоеДеревоПоОбразцу(Источник);
СкопироватьСтрокиДерева(Источник.Строки, Приемник.Строки);
// 2. Дописать строки в конец уже заполненного дерева
СкопироватьСтрокиДерева(Источник.Строки, СуществующееДерево.Строки);
// 3. Вставить всё дерево под конкретный узел приёмника
УзелПриемника = СуществующееДерево.Строки[2];
СкопироватьСтрокиДерева(Источник.Строки, УзелПриемника.Строки);
Копирование одной ветки вместе с её корнем
Чтобы перенести одну строку со всеми подчинёнными, сначала копируют саму строку, потом её детей:
Процедура СкопироватьВетку(СтрокаИсточник, СтрокиПриемник) Экспорт
НоваяСтрока = СтрокиПриемник.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
СкопироватьСтрокиДерева(СтрокаИсточник.Строки, НоваяСтрока.Строки);
КонецПроцедуры
Глубокая копия значений в ячейках
Если в колонках лежат массивы или структуры, а копия должна быть полностью независимой, значение ячейки копируют отдельно. В типовых конфигурациях на БСП для этого есть функция ОбщегоНазначения.СкопироватьРекурсивно() — она дублирует структуры, соответствия, массивы и списки значений вместе с вложенными коллекциями.
Ещё один способ сделать полностью независимую копию всего дерева — сериализация во внутреннюю строку:
ДеревоКопия = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(ДеревоИсходное));
Функции ЗначениеВСтрокуВнутр и ЗначениеИзСтрокиВнутр недоступны в тонком клиенте, поэтому такой код размещают на сервере. На больших деревьях способ заметно медленнее Скопировать() и требует памяти на промежуточную строку, так что используют его, только когда глубокая копия действительно нужна.
Копирование дерева значений с изменением структуры
Ручной обход хорош тем, что во время копирования можно менять данные. Три типичных случая.
Другие имена колонок
Если в приёмнике колонки названы иначе, ЗаполнитьЗначенияСвойств их не сопоставит. Передайте соответствие имён:
// Соответствие: ключ — имя колонки источника, значение — имя колонки приёмника
Процедура СкопироватьСПереименованием(СтрокиИсточник, СтрокиПриемник, ИменаКолонок)
Для Каждого СтрокаИсточник Из СтрокиИсточник Цикл
НоваяСтрока = СтрокиПриемник.Добавить();
Для Каждого Пара Из ИменаКолонок Цикл
НоваяСтрока[Пара.Значение] = СтрокаИсточник[Пара.Ключ];
КонецЦикла;
СкопироватьСПереименованием(СтрокаИсточник.Строки, НоваяСтрока.Строки, ИменаКолонок);
КонецЦикла;
КонецПроцедуры
ИменаКолонок = Новый Соответствие;
ИменаКолонок.Вставить("Номенклатура", "Товар");
ИменаКолонок.Вставить("Сумма", "СуммаПродаж");
Копирование с отбором строк
Задача: перенести только строки с суммой от 10 000, но сохранить их родителей, чтобы иерархия не развалилась. Функция возвращает Истина, если в ветке осталась хотя бы одна подходящая строка:
Функция СкопироватьСОтбором(СтрокиИсточник, СтрокиПриемник, МинСумма)
ЕстьПодходящие = Ложь;
Для Каждого СтрокаИсточник Из СтрокиИсточник Цикл
НоваяСтрока = СтрокиПриемник.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
ЕстьПотомки = СкопироватьСОтбором(СтрокаИсточник.Строки, НоваяСтрока.Строки, МинСумма);
Если ЕстьПотомки Или СтрокаИсточник.Сумма >= МинСумма Тогда
ЕстьПодходящие = Истина;
Иначе
СтрокиПриемник.Удалить(НоваяСтрока);
КонецЕсли;
КонецЦикла;
Возврат ЕстьПодходящие;
КонецФункции
Удаляется строка приёмника, а не источника, поэтому перебор Для Каждого по источнику не ломается.
Из дерева — в плоскую таблицу
Иногда «другое дерево» на деле нужно в виде таблицы с номером уровня, например для выгрузки в Excel. Метод Уровень() строки дерева возвращает глубину: 0 у строк верхнего уровня.
Процедура ДеревоВТаблицу(СтрокиИсточник, Таблица)
Для Каждого СтрокаИсточник Из СтрокиИсточник Цикл
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
НоваяСтрока.Уровень = СтрокаИсточник.Уровень();
ДеревоВТаблицу(СтрокаИсточник.Строки, Таблица);
КонецЦикла;
КонецПроцедуры
Колонку Уровень в таблице создайте заранее с типом Число.
Дерево на управляемой форме: копирование реквизитов
На форме реквизит типа «Дерево значений» живёт как ДанныеФормыДерево. Это другой тип: у него нет Строки, вместо них метод ПолучитьЭлементы(), и нет метода Скопировать(). Сам тип ДеревоЗначений в тонком клиенте недоступен.
| Задача | Где выполнять | Как |
|---|---|---|
| Скопировать один реквизит формы в другой с той же структурой | клиент или сервер | КопироватьДанныеФормы(ДеревоИсточник, ДеревоПриемник) |
| Скопировать с отбором или сменой колонок | сервер | РеквизитФормыВЗначение() → обработка → ЗначениеВРеквизитФормы() |
| Перенести строки без серверного вызова | клиент | рекурсия по ПолучитьЭлементы() |
Серверный вариант через обычное дерево:
&НаСервере
Процедура СкопироватьДеревоНаСервере()
Источник = РеквизитФормыВЗначение("ДеревоИсточник");
Приемник = РеквизитФормыВЗначение("ДеревоПриемник");
Приемник.Строки.Очистить();
СкопироватьСтрокиДерева(Источник.Строки, Приемник.Строки);
ЗначениеВРеквизитФормы(Приемник, "ДеревоПриемник");
КонецПроцедуры
Клиентский вариант без обращения к серверу:
&НаКлиенте
Процедура СкопироватьЭлементыДерева(ЭлементыИсточник, ЭлементыПриемник)
Для Каждого Элемент Из ЭлементыИсточник Цикл
НовыйЭлемент = ЭлементыПриемник.Добавить();
ЗаполнитьЗначенияСвойств(НовыйЭлемент, Элемент);
СкопироватьЭлементыДерева(Элемент.ПолучитьЭлементы(), НовыйЭлемент.ПолучитьЭлементы());
КонецЦикла;
КонецПроцедуры
// Вызов:
// ДеревоПриемник.ПолучитьЭлементы().Очистить();
// СкопироватьЭлементыДерева(ДеревоИсточник.ПолучитьЭлементы(), ДеревоПриемник.ПолучитьЭлементы());
Колонки реквизита формы задаются в конструкторе формы или программно через ИзменитьРеквизиты(). Если в приёмнике нет нужной колонки, ЗаполнитьЗначенияСвойств молча пропустит её — данные «пропадут» без всякой ошибки.
Большие деревья: как копировать быстро
На дереве в сотни тысяч строк разница между подходами становится ощутимой. Что реально влияет на скорость:
- 🚀 Используйте
Скопировать(), когда он подходит. Встроенный метод работает внутри платформы и обгоняет любой цикл на встроенном языке. - 🚀 Стройте дерево сразу из запроса. Если исходник — результат запроса, вместо «выгрузить и скопировать» выгрузите нужную структуру сразу:
РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам)с итогамиПО ИЕРАРХИИили по нужным полям в тексте запроса. - 🚀 Ограничьте список колонок. Третий параметр
ЗаполнитьЗначенияСвойствпринимает строку с именами через запятую:ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник, "Номенклатура, Количество, Сумма"). Платформе не приходится сопоставлять лишние поля. - 🚀 Не ищите строки внутри цикла копирования. Вызов
Строки.Найти(Значение, "Колонка", Истина)просматривает всё дерево. Если при копировании нужно находить узлы приёмника по ключу, заведитеСоответствие«ключ → строка» и заполняйте его по мере добавления. - 🚀 Не гоняйте дерево между клиентом и сервером построчно. Один серверный вызов, в котором копируется всё дерево, быстрее сотни вызовов по строке.
- 🚀 Помните о памяти. Копия удваивает объём данных. На сервере с ограниченной памятью под рабочий процесс лучше обработать дерево на месте или освободить исходник (
Источник = Неопределено;) сразу после копирования.
Обход без рекурсии для очень глубоких деревьев
Рекурсия расходует стек вызовов на каждый уровень. Для обычных деревьев в 5–20 уровней это не проблема. Для данных с глубиной в тысячи уровней (например, цепочки «родитель — потомок» из внешней системы) надёжнее обход со стеком на массиве:
Процедура СкопироватьДеревоБезРекурсии(Источник, Приемник)
Стек = Новый Массив;
Стек.Добавить(Новый Структура("Откуда, Куда", Источник.Строки, Приемник.Строки));
Пока Стек.Количество() > 0 Цикл
Индекс = Стек.ВГраница();
Пара = Стек[Индекс];
Стек.Удалить(Индекс);
Для Каждого СтрокаИсточник Из Пара.Откуда Цикл
НоваяСтрока = Пара.Куда.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
Если СтрокаИсточник.Строки.Количество() > 0 Тогда
Стек.Добавить(Новый Структура("Откуда, Куда", СтрокаИсточник.Строки, НоваяСтрока.Строки));
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Порядок строк внутри каждого родителя сохраняется: подчинённые строки всегда добавляются в коллекцию своего узла, а не в общий список.
Приемник = Источник.Скопировать(); — самый быстрый вариант.ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(Источник)) на сервере или Скопировать() плюс копирование вложенных коллекций.Строки.Добавить() и ЗаполнитьЗначенияСвойств().КопироватьДанныеФормы() при одинаковой структуре, иначе серверная обработка через РеквизитФормыВЗначение().Частые ошибки при копировании дерева и как их исправить
Копия меняется вместе с оригиналом. Вместо копии сделано присваивание или в ячейках лежат общие массивы и структуры. Проверьте, где создаётся приёмник: должно быть Скопировать(), Новый ДеревоЗначений или сериализация, а не знак =.
Поле объекта не обнаружено (ИмяКолонки). Код обращается к колонке, которой нет в приёмнике или источнике: НоваяСтрока.Сумма = ... при отсутствии колонки Сумма. Создайте колонки до копирования или используйте ЗаполнитьЗначенияСвойств, который пропускает несовпадающие имена.
После копирования в приёмнике пустые значения. Обратная сторона той же медали: колонки названы иначе или в имени затесалась латинская буква (Контрагент и Kонтрагент с латинской «K» выглядят одинаково, но это разные имена). Сравните имена в отладчике или скопируйте с явным соответствием имён.
Бесконечный цикл или зависание. Копирование дерева «в самого себя»: источник и приёмник — один объект, и обход постоянно натыкается на только что добавленные строки. Для дублирования ветки внутри одного дерева сначала сделайте копию через Скопировать(), а потом переносите строки из копии.
Ошибка типа на клиенте. ДеревоЗначений создаётся или передаётся в процедуру с директивой &НаКлиенте в тонком клиенте. Перенесите работу на сервер или используйте ДанныеФормыДерево и ПолучитьЭлементы().
Плоское дерево вместо иерархии. Все строки добавляются в Приемник.Строки, а не в НоваяСтрока.Строки. Проверьте, что рекурсивный вызов получает коллекцию подчинённых строк новой строки.
Частые вопросы
Сохраняются ли типы колонок при Скопировать()?
Можно ли скопировать дерево значений в таблицу значений одной командой?
Уровень().Как скопировать только колонки, без строк?
Источник.Колонки, передавая имя, тип, заголовок и ширину. Для небольших деревьев проще Скопировать() и затем Строки.Очистить().Попадут ли в копию ссылки на элементы справочников?
Можно ли сохранить копию дерева между сеансами?
ХранилищеЗначения и записать в реквизит типа «Хранилище значения» либо сохранить в хранилище настроек. При чтении получите независимый объект.