Независимая копия дерева значений в 1С 8.3: от метода Скопировать() до своей рекурсии

· 9 мин

Содержание

Самый короткий способ скопировать дерево значений в 1С — вызвать Копия = Дерево.Скопировать(); на сервере. Получится новое дерево с теми же колонками, строками и иерархией, и правки в копии не затронут оригинал.

Если нужна копия не всего дерева, а одной ветки, части колонок или строк по условию, пишут свою рекурсивную процедуру. Когда в ячейках лежат структуры и массивы и их тоже нужно отвязать от оригинала, помогает сериализация. Все пять способов ниже даны с готовым кодом, который можно вставить в общий модуль.

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

ДеревоЗначений — объект, а не простое значение. Строка Копия = Дерево; не создаёт второе дерево. Она кладёт в переменную Копия ссылку на тот же самый объект.

Проверить это легко:

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

Копия = Дерево;
Копия.Строки[0].Наименование = "Изменено";

Сообщить(Дерево.Строки[0].Наименование); // выведет «Изменено»

Отдельная копия нужна в нескольких типовых ситуациях:

  • 🔁 пользователь правит дерево на форме, а исходное состояние нужно сохранить для кнопки «Отменить»;
  • 🧪 алгоритм расчёта меняет или сворачивает строки, а исходные данные ещё понадобятся;
  • 📊 одно дерево из запроса нужно показать в двух местах с разной фильтрацией;
  • 🧩 функция получает дерево параметром и не должна менять данные вызывающего кода.

Ещё одна деталь. Дерево значений существует только на сервере, в толстом клиенте и во внешнем соединении. В тонком клиенте и веб-клиенте его нет: код Новый ДеревоЗначений в клиентской процедуре управляемой формы упадёт с ошибкой Тип не определен (ДеревоЗначений). Поэтому все примеры ниже выполняются в серверных процедурах или в общем модуле с флажком «Сервер».

Какой способ копирования выбрать: сравнение

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

СпособКогда подходитЧто копируетОграничения
Скопировать()Нужна полная копия как естьКолонки, все строки, иерархиюОбъекты в ячейках (массивы, структуры) остаются общими
Рекурсия с ЗаполнитьЗначенияСвойствНужна часть дерева: ветка, колонки, фильтрТо, что вы явно перенесётеБольше кода, медленнее встроенного метода
ЗначениеВСтрокуВнутрНужна глубокая копия вместе с вложенными объектамиВсё, включая содержимое ячеекЗначения в ячейках должны поддерживать сериализацию
XML или JSON через СериализаторXDTOДерево надо сохранить, передать или восстановить позжеКолонки, строки, значенияМедленнее, объёмный текст, только XDTO-совместимые типы
Копия реквизита формыДерево живёт на управляемой формеДанные реквизита формыКолонки реквизитов должны совпадать по именам

Если сомневаетесь, начните со Скопировать(). К рекурсии переходите, когда понадобится выбирать, что именно копировать.

Способ 1. Встроенный метод Скопировать()

У объекта ДеревоЗначений есть метод Скопировать(). Он возвращает новое дерево со всеми колонками (имя, тип, заголовок, ширина) и всеми строками на всех уровнях вложенности.

&НаСервере
Процедура ПересчитатьБезПотериИсходных()

	ИсходноеДерево = ПолучитьДеревоИзЗапроса();
	РабочееДерево  = ИсходноеДерево.Скопировать();

	// Правим копию, оригинал не меняется
	Для Каждого СтрокаВерхнегоУровня Из РабочееДерево.Строки Цикл
		СтрокаВерхнегоУровня.Строки.Очистить();
	КонецЦикла;

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

Этот вариант самый быстрый: копирование идёт внутри платформы, без цикла на встроенном языке.

Конструктора-копировщика у дерева нет. Запись Новый ДеревоЗначений(ИсходноеДерево) не сработает: конструктор не принимает параметров. Копию получают только через метод или свой код.

Скопировать() переносит значения ячеек «как есть». Если в ячейке лежит Массив или Структура, в обоих деревьях окажется ссылка на один и тот же массив. Добавили элемент в массив копии — он появится и в оригинале. Для таких данных используйте способ 3.

Способ 2. Рекурсивное копирование дерева значений с сохранением иерархии

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

Полная копия рекурсией

Функция КопияДереваЗначений(Источник) Экспорт

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

	СкопироватьСтрокиДерева(Источник.Строки, Приемник.Строки);
	Возврат Приемник;

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

Процедура СкопироватьСтрокиДерева(СтрокиИсточника, СтрокиПриемника)

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

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

ЗаполнитьЗначенияСвойств переносит значения всех одноимённых колонок одной строкой кода. Рекурсивный вызов для СтрокаИсточника.Строки сохраняет иерархию: подчинённые строки попадают ровно под своего родителя.

Только нужные колонки

Третий параметр ЗаполнитьЗначенияСвойств — список свойств через запятую. В приёмнике создайте только эти колонки:

КолонкиКопии = "Номенклатура,Количество,Сумма";

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

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

Одна ветка

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

Функция КопияВетки(Источник, СтрокаВетки) Экспорт

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

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

	Возврат Приемник;

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

Строки по условию

Фильтр ставят прямо в цикл обхода. Пример: пропустить строки с нулевой суммой вместе со всеми их детьми.

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

Если детей нужно сохранить и при отброшенном родителе, переносите их на уровень выше: вызывайте рекурсию для СтрокаИсточника.Строки с тем же СтрокиПриемника.

Способ 3. Копирование дерева через сериализацию

Сериализация превращает дерево в текст и собирает из текста новый объект. В результате копируются не только строки, но и объекты внутри ячеек: массивы, структуры, вложенные таблицы. Это глубокая копия.

ЗначениеВСтрокуВнутр — быстрая глубокая копия

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

Одна строка, и в копии нет ни одного общего объекта с оригиналом. Внутренний формат 1С компактный и разбирается платформой, поэтому способ работает заметно быстрее XML.

Ограничение: значения в ячейках должны поддерживать такую сериализацию. Примитивы, ссылки на объекты базы, массивы, структуры, таблицы значений проходят. Объекты вроде ЗаписьXML, HTTPСоединение или COM-объекты положить в строку нельзя, получите ошибку выполнения.

XML через СериализаторXDTO

Подходит, когда дерево нужно не только скопировать, но и сохранить в файл, в реквизит типа «Строка» или передать в другую базу.

// Запись дерева в текст XML
Запись = Новый ЗаписьXML;
Запись.УстановитьСтроку();
СериализаторXDTO.ЗаписатьXML(Запись, Дерево, НазначениеТипаXML.Явное);
ТекстXML = Запись.Закрыть();

// Восстановление копии
Чтение = Новый ЧтениеXML;
Чтение.УстановитьСтроку(ТекстXML);
Копия = СериализаторXDTO.ПрочитатьXML(Чтение);
Чтение.Закрыть();

НазначениеТипаXML.Явное записывает тип значения в каждый элемент. Без него при чтении платформа может не понять, что перед ней дерево значений.

JSON

С JSON есть ловушка. Глобальная процедура ЗаписатьJSON дерево значений не принимает, она работает со структурами, массивами, соответствиями и примитивами. Дерево в JSON пишет СериализаторXDTO:

Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
СериализаторXDTO.ЗаписатьJSON(Запись, Дерево, НазначениеТипаXML.Явное);
ТекстJSON = Запись.Закрыть();

Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(ТекстJSON);
Копия = СериализаторXDTO.ПрочитатьJSON(Чтение);
Чтение.Закрыть();

Получившийся JSON имеет служебную разметку XDTO. Внешней системе он обычно не подходит. Если нужен «чистый» JSON для обмена, сначала переложите дерево рекурсией в массив структур, а потом запишите массив.

Способ 4. Копирование дерева значений на управляемой форме

На форме дерево хранится не как ДеревоЗначений, а как реквизит типа ДанныеФормыДерево. Методов дерева значений у него нет, но есть два рабочих пути.

Через сервер

РеквизитФормыВЗначение уже создаёт новый объект ДеревоЗначений, не связанный с формой. Достаточно загрузить его во второй реквизит:

&НаСервере
Процедура СохранитьИсходноеСостояниеНаСервере()

	ЗначениеВРеквизитФормы(РеквизитФормыВЗначение("ДеревоДанных"), "ДеревоИсходное");

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

Оба реквизита формы должны иметь колонки с одинаковыми именами. Колонки, которых нет в приёмнике, просто не перенесутся.

Без вызова сервера

На клиенте подойдёт глобальный метод КопироватьДанныеФормы(Источник, Приемник):

&НаКлиенте
Процедура Отменить(Команда)

	КопироватьДанныеФормы(ДеревоИсходное, ДеревоДанных);

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

Источник и приёмник должны быть реквизитами формы одного вида — оба ДанныеФормыДерево с одинаковым набором колонок.

Можно ли скопировать дерево вручную

Без кода полностью скопировать дерево не получится: в режиме «1С:Предприятие» нет команды, которая дублирует ветку со всеми подчинёнными строками. Стандартное копирование строки на форме (F9) создаёт одну строку без детей.

Для отладки и разовой проверки есть обходной путь. Остановитесь в отладчике на нужной строке кода, откройте окно вычисления выражения (Shift+F9) и выполните Дерево.Скопировать(). Результат можно раскрыть и посмотреть, не меняя исходную переменную.

Способ 5. Как быстро копировать большие деревья значений

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

  1. 🥇 Скопировать() — быстрее всего, цикл идёт внутри платформы.
  2. 🥈 ЗначениеВСтрокуВнутр — немного медленнее, зато даёт глубокую копию.
  3. 🥉 Рекурсия на встроенном языке — каждая строка обрабатывается интерпретатором.
  4. XML и JSON — самые медленные из-за разбора текста и объёма строки.

Если без рекурсии не обойтись, ускорьте её:

  • ⚡ Передавайте список колонок в ЗаполнитьЗначенияСвойств. Чем меньше колонок, тем меньше работы на каждой строке.
  • ⚡ Не вызывайте внутри цикла Строки.Найти() или НайтиСтроки() по всему дереву. На каждой строке это полный обход, и время растёт квадратично.
  • ⚡ Отбрасывайте ненужные ветки до рекурсивного вызова, а не после. Тогда платформа не обходит их детей.
  • ⚡ Не делайте копию в цикле «на всякий случай». Одна копия до цикла дешевле сотни копий внутри.

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

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

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

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

Ещё одна частая ловушка — удаление строк из дерева внутри цикла Для Каждого по тем же строкам. Часть строк при этом пропускается. Удаляйте строки в обратном цикле по индексу:

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

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

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