Независимая копия дерева значений в 1С 8.3: от метода Скопировать() до своей рекурсии
Содержание
Самый короткий способ скопировать дерево значений в 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. Как быстро копировать большие деревья значений
На деревьях в несколько десятков тысяч строк разница между способами становится заметной. Порядок такой:
- 🥇
Скопировать()— быстрее всего, цикл идёт внутри платформы. - 🥈
ЗначениеВСтрокуВнутр— немного медленнее, зато даёт глубокую копию. - 🥉 Рекурсия на встроенном языке — каждая строка обрабатывается интерпретатором.
- XML и JSON — самые медленные из-за разбора текста и объёма строки.
Если без рекурсии не обойтись, ускорьте её:
- ⚡ Передавайте список колонок в
ЗаполнитьЗначенияСвойств. Чем меньше колонок, тем меньше работы на каждой строке. - ⚡ Не вызывайте внутри цикла
Строки.Найти()илиНайтиСтроки()по всему дереву. На каждой строке это полный обход, и время растёт квадратично. - ⚡ Отбрасывайте ненужные ветки до рекурсивного вызова, а не после. Тогда платформа не обходит их детей.
- ⚡ Не делайте копию в цикле «на всякий случай». Одна копия до цикла дешевле сотни копий внутри.
Часто копию можно не делать вовсе. Если дерево получено из запроса через РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам), второй вызов Выгрузить() у того же результата вернёт новое независимое дерево. Повторно выполнять запрос для этого не нужно.
Глубина рекурсии у деревьев из запросов обычно небольшая, несколько уровней группировок. Переполнения стека можно опасаться только на самодельных деревьях с тысячами уровней вложенности.
Ошибки при копировании дерева значений
Копия = Дерево. Замените его на Дерево.Скопировать(). Если присваивания нет, а меняется содержимое ячейки-массива или структуры, нужна глубокая копия через ЗначениеВСтрокуВнутр.&НаСервере или в серверный общий модуль.ЗаполнитьЗначенияСвойств молча пропускает колонки, которых нет в приёмнике, ошибки не будет. Сверьте имена колонок.СтрокаИсточника.Строки, либо подчинённые строки добавляются в Приемник.Строки вместо НоваяСтрока.Строки.Скопировать().ДанныеФормыДерево. Получите дерево значений через РеквизитФормыВЗначение или используйте КопироватьДанныеФормы.Ещё одна частая ловушка — удаление строк из дерева внутри цикла Для Каждого по тем же строкам. Часть строк при этом пропускается. Удаляйте строки в обратном цикле по индексу:
Для Сч = 1 По Строки.Количество() Цикл
Строка = Строки[Строки.Количество() - Сч];
Если Строка.Сумма = 0 Тогда
Строки.Удалить(Строка);
КонецЕсли;
КонецЦикла;
Частые вопросы о копировании дерева значений
Копирует ли Скопировать() итоги по колонкам?
Итог() в момент вызова. В копии он вернёт те же суммы, пока вы не измените строки.Как скопировать дерево значений в таблицу значений?
ЗаполнитьЗначенияСвойств. Уровень вложенности при необходимости сохраняйте в отдельной колонке.Можно ли передать дерево значений с сервера на клиент?
ДанныеФормыДерево или сериализованную строку XML.Чем копия через ЗначениеВСтрокуВнутр отличается от Скопировать()?
Скопировать() создаёт новые строки, но объекты в ячейках остаются общими. ЗначениеВСтрокуВнутр пересоздаёт и их, поэтому копия полностью независима.Сохраняется ли тип колонок при копировании?
Колонка.ТипЗначения, как в примерах выше. Скопировать() и сериализация переносят типы сами.