Как перенести дерево значений в другое дерево и не испортить исходник

· 10 мин

Содержание

Чтобы скопировать дерево значений в 1С целиком, вызовите ДеревоКопия = Дерево.Скопировать(); — получится новое независимое дерево с теми же колонками и всей иерархией строк. Если нужно перенести строки в уже существующее дерево, под конкретный узел или с отбором, пишут рекурсивную процедуру: на каждом уровне добавляют строку через Строки.Добавить(), заполняют её ЗаполнитьЗначенияСвойств() и спускаются в подчинённые строки.

Типичная ситуация: разработчик строит в отчёте или обработке дерево «Контрагент → Договор → Документ», хочет сохранить «снимок» до пересчёта, пишет Снимок = Дерево; — и через минуту видит, что снимок пересчитался вместе с оригиналом. Ниже разобраны все рабочие способы копирования для платформы 8.3: встроенный метод, рекурсия, обход без рекурсии, копирование со сменой структуры и работа с деревьями на управляемой форме.

Почему присваивание не создаёт копию дерева

ДеревоЗначений — объект, а переменная хранит ссылку на него. Строка Дерево2 = Дерево1; не копирует данные, а даёт второе имя тому же объекту в памяти.

Дерево2 = Дерево1;
Дерево2.Строки[0].Сумма = 0;
// Дерево1.Строки[0].Сумма теперь тоже 0 — это один и тот же объект

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

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

Метод Скопировать() для дерева значений и его ограничения

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

ДеревоКопия = ДеревоИсходное.Скопировать();
ДеревоКопия.Строки[0].Сумма = 0; // исходное дерево не изменилось

Что делает метод:

  • ✅ создаёт новый объект ДеревоЗначений;
  • ✅ переносит все колонки с типами, заголовками и шириной;
  • ✅ копирует все строки на всех уровнях с сохранением порядка и иерархии.

Где его возможностей не хватает:

  • ❌ нельзя скопировать в существующее дерево. Метод всегда возвращает новый объект. Если приёмник уже заполнен или привязан к чему-то (например, передан в другую процедуру), Скопировать() не подходит;
  • ❌ нельзя выбрать часть строк или колонок. В отличие от ТаблицаЗначений.Скопировать(Строки, Колонки), у дерева метод копирует всё;
  • ❌ нельзя скопировать одну ветку. У строки дерева (СтрокаДереваЗначений) метода копирования нет;
  • ❌ вложенные объекты в ячейках не дублируются. Если в колонке лежит массив, структура или таблица значений, копия получит ссылку на тот же объект. Меняете массив в копии — меняется и в исходнике.

Ссылки на справочники и документы (СправочникСсылка, ДокументСсылка) копировать «глубоко» не нужно: ссылка неизменяема, её копия ничем не хуже оригинала. Проблема возникает только с изменяемыми коллекциями внутри ячеек.

Пустое дерево с той же структурой колонок

Частая подзадача — получить приёмник с такими же колонками, но без строк. Удобный приём:

Приемник = Источник.Скопировать();
Приемник.Строки.Очистить();

На больших деревьях это расточительно: сначала копируются все строки, потом удаляются. Тогда колонки переносят циклом:

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

Рекурсивный обход: копируем строки в другое дерево вручную

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

Алгоритм на каждом уровне:

  1. Перебрать строки источника текущего уровня.
  2. Для каждой добавить строку в коллекцию приёмника: СтрокиПриемник.Добавить().
  3. Заполнить её значения из строки источника: ЗаполнитьЗначенияСвойств().
  4. Вызвать ту же процедуру для подчинённых строк обеих сторон.

ЗаполнитьЗначенияСвойств(Приемник, Источник) переносит значения колонок с совпадающими именами. Колонки, которых нет в приёмнике, пропускаются без ошибки. Это и делает ручное копирование гибким: приёмник может иметь другой набор колонок.

Пример кода рекурсивного копирования дерева значений

Базовая процедура. Она работает в любом контексте, где доступен тип ДеревоЗначений: на сервере, в толстом клиенте, во внешнем соединении.

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

Варианты вызова под разные задачи:

// 1. Всё дерево целиком в новое дерево с такими же колонками
Приемник = НовоеДеревоПоОбразцу(Источник);
СкопироватьСтрокиДерева(Источник.Строки, Приемник.Строки);

// 2. Дописать строки в конец уже заполненного дерева
СкопироватьСтрокиДерева(Источник.Строки, СуществующееДерево.Строки);

// 3. Вставить всё дерево под конкретный узел приёмника
УзелПриемника = СуществующееДерево.Строки[2];
СкопироватьСтрокиДерева(Источник.Строки, УзелПриемника.Строки);

Копирование одной ветки вместе с её корнем

Чтобы перенести одну строку со всеми подчинёнными, сначала копируют саму строку, потом её детей:

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

Глубокая копия значений в ячейках

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

Ещё один способ сделать полностью независимую копию всего дерева — сериализация во внутреннюю строку:

ДеревоКопия = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(ДеревоИсходное));

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

Копирование дерева значений с изменением структуры

Ручной обход хорош тем, что во время копирования можно менять данные. Три типичных случая.

Другие имена колонок

Если в приёмнике колонки названы иначе, ЗаполнитьЗначенияСвойств их не сопоставит. Передайте соответствие имён:

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

ИменаКолонок = Новый Соответствие;
ИменаКолонок.Вставить("Номенклатура", "Товар");
ИменаКолонок.Вставить("Сумма", "СуммаПродаж");

Копирование с отбором строк

Задача: перенести только строки с суммой от 10 000, но сохранить их родителей, чтобы иерархия не развалилась. Функция возвращает Истина, если в ветке осталась хотя бы одна подходящая строка:

Функция СкопироватьСОтбором(СтрокиИсточник, СтрокиПриемник, МинСумма)
	ЕстьПодходящие = Ложь;
	Для Каждого СтрокаИсточник Из СтрокиИсточник Цикл
		НоваяСтрока = СтрокиПриемник.Добавить();
		ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
		ЕстьПотомки = СкопироватьСОтбором(СтрокаИсточник.Строки, НоваяСтрока.Строки, МинСумма);
		Если ЕстьПотомки Или СтрокаИсточник.Сумма >= МинСумма Тогда
			ЕстьПодходящие = Истина;
		Иначе
			СтрокиПриемник.Удалить(НоваяСтрока);
		КонецЕсли;
	КонецЦикла;
	Возврат ЕстьПодходящие;
КонецФункции

Удаляется строка приёмника, а не источника, поэтому перебор Для Каждого по источнику не ломается.

Из дерева — в плоскую таблицу

Иногда «другое дерево» на деле нужно в виде таблицы с номером уровня, например для выгрузки в Excel. Метод Уровень() строки дерева возвращает глубину: 0 у строк верхнего уровня.

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

Колонку Уровень в таблице создайте заранее с типом Число.

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

На форме реквизит типа «Дерево значений» живёт как ДанныеФормыДерево. Это другой тип: у него нет Строки, вместо них метод ПолучитьЭлементы(), и нет метода Скопировать(). Сам тип ДеревоЗначений в тонком клиенте недоступен.

ЗадачаГде выполнятьКак
Скопировать один реквизит формы в другой с той же структуройклиент или серверКопироватьДанныеФормы(ДеревоИсточник, ДеревоПриемник)
Скопировать с отбором или сменой колоноксерверРеквизитФормыВЗначение() → обработка → ЗначениеВРеквизитФормы()
Перенести строки без серверного вызоваклиентрекурсия по ПолучитьЭлементы()

Серверный вариант через обычное дерево:

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

Клиентский вариант без обращения к серверу:

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

// Вызов:
// ДеревоПриемник.ПолучитьЭлементы().Очистить();
// СкопироватьЭлементыДерева(ДеревоИсточник.ПолучитьЭлементы(), ДеревоПриемник.ПолучитьЭлементы());

Колонки реквизита формы задаются в конструкторе формы или программно через ИзменитьРеквизиты(). Если в приёмнике нет нужной колонки, ЗаполнитьЗначенияСвойств молча пропустит её — данные «пропадут» без всякой ошибки.

Большие деревья: как копировать быстро

На дереве в сотни тысяч строк разница между подходами становится ощутимой. Что реально влияет на скорость:

  • 🚀 Используйте Скопировать(), когда он подходит. Встроенный метод работает внутри платформы и обгоняет любой цикл на встроенном языке.
  • 🚀 Стройте дерево сразу из запроса. Если исходник — результат запроса, вместо «выгрузить и скопировать» выгрузите нужную структуру сразу: РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) с итогами ПО ИЕРАРХИИ или по нужным полям в тексте запроса.
  • 🚀 Ограничьте список колонок. Третий параметр ЗаполнитьЗначенияСвойств принимает строку с именами через запятую: ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник, "Номенклатура, Количество, Сумма"). Платформе не приходится сопоставлять лишние поля.
  • 🚀 Не ищите строки внутри цикла копирования. Вызов Строки.Найти(Значение, "Колонка", Истина) просматривает всё дерево. Если при копировании нужно находить узлы приёмника по ключу, заведите Соответствие «ключ → строка» и заполняйте его по мере добавления.
  • 🚀 Не гоняйте дерево между клиентом и сервером построчно. Один серверный вызов, в котором копируется всё дерево, быстрее сотни вызовов по строке.
  • 🚀 Помните о памяти. Копия удваивает объём данных. На сервере с ограниченной памятью под рабочий процесс лучше обработать дерево на месте или освободить исходник (Источник = Неопределено;) сразу после копирования.

Обход без рекурсии для очень глубоких деревьев

Рекурсия расходует стек вызовов на каждый уровень. Для обычных деревьев в 5–20 уровней это не проблема. Для данных с глубиной в тысячи уровней (например, цепочки «родитель — потомок» из внешней системы) надёжнее обход со стеком на массиве:

Процедура СкопироватьДеревоБезРекурсии(Источник, Приемник)
	Стек = Новый Массив;
	Стек.Добавить(Новый Структура("Откуда, Куда", Источник.Строки, Приемник.Строки));
	Пока Стек.Количество() > 0 Цикл
		Индекс = Стек.ВГраница();
		Пара = Стек[Индекс];
		Стек.Удалить(Индекс);
		Для Каждого СтрокаИсточник Из Пара.Откуда Цикл
			НоваяСтрока = Пара.Куда.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
			Если СтрокаИсточник.Строки.Количество() > 0 Тогда
				Стек.Добавить(Новый Структура("Откуда, Куда", СтрокаИсточник.Строки, НоваяСтрока.Строки));
			КонецЕсли;
		КонецЦикла;
	КонецЦикла;
КонецПроцедуры

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

Какой способ копирования дерева вам нужен?

Частые ошибки при копировании дерева и как их исправить

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

Поле объекта не обнаружено (ИмяКолонки). Код обращается к колонке, которой нет в приёмнике или источнике: НоваяСтрока.Сумма = ... при отсутствии колонки Сумма. Создайте колонки до копирования или используйте ЗаполнитьЗначенияСвойств, который пропускает несовпадающие имена.

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

Бесконечный цикл или зависание. Копирование дерева «в самого себя»: источник и приёмник — один объект, и обход постоянно натыкается на только что добавленные строки. Для дублирования ветки внутри одного дерева сначала сделайте копию через Скопировать(), а потом переносите строки из копии.

Ошибка типа на клиенте. ДеревоЗначений создаётся или передаётся в процедуру с директивой &НаКлиенте в тонком клиенте. Перенесите работу на сервер или используйте ДанныеФормыДерево и ПолучитьЭлементы().

Плоское дерево вместо иерархии. Все строки добавляются в Приемник.Строки, а не в НоваяСтрока.Строки. Проверьте, что рекурсивный вызов получает коллекцию подчинённых строк новой строки.

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

Сохраняются ли типы колонок при Скопировать()?
Да. Метод переносит колонки вместе с описанием типов, заголовками и шириной, поэтому копия ведёт себя так же, как исходник при вводе и сравнении значений.
Можно ли скопировать дерево значений в таблицу значений одной командой?
Нет, встроенного метода для этого нет. Нужен обход дерева с добавлением строк в таблицу; уровень вложенности сохраняют в отдельной колонке через метод Уровень().
Как скопировать только колонки, без строк?
Создайте новое дерево и добавьте в него колонки циклом по Источник.Колонки, передавая имя, тип, заголовок и ширину. Для небольших деревьев проще Скопировать() и затем Строки.Очистить().
Попадут ли в копию ссылки на элементы справочников?
Да, ссылка копируется как обычное значение. Это безопасно: ссылка неизменяема и указывает на тот же элемент базы, сам элемент при этом не дублируется.
Можно ли сохранить копию дерева между сеансами?
Да. Дерево значений сериализуется: его можно поместить в ХранилищеЗначения и записать в реквизит типа «Хранилище значения» либо сохранить в хранилище настроек. При чтении получите независимый объект.