Как скопировать структуру или массив «по значению» в 1С и избежать изменения исходных данных?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке на платформе 1С:Предприятие программисты регулярно сталкиваются с необходимостью создания объектов на основе предопределенных шаблонов. Типичный сценарий: подготовить структуру-шаблон со значениями по умолчанию (например, для последующей выгрузки в JSON или XML), а затем в цикле создавать новые экземпляры этой структуры, заполнять их уникальными данными и складывать в результирующий массив.

Однако на практике разработчики часто наступают на скрытые «грабли» платформы: при изменении данных в одном из созданных экземпляров эти же данные синхронно меняются во всех остальных копиях. Изменение вложенного массива в одной структуре волшебным образом приводит к перезаписи данных во всех элементах результирующего списка. Давайте подробно разберем, почему так происходит, как устроена работа с примитивными и ссылочными типами данных в 1С, и какими способами можно выполнить настоящее «глубокое копирование» объектов.

Выясняем причину: как 1С работает с переменными в памяти

Для того чтобы понять природу этой проблемы, нам необходимо проанализировать, каким образом платформа 1С хранит переменные различных типов данных в оперативной памяти компьютера. Все типы данных в 1С можно разделить на две принципиально разные категории:

  1. Примитивные типы данных. К ним относятся Число, Строка, Дата и Булево. Переменная примитивного типа хранит непосредственно само значение.
  2. Ссылочные типы и сложные объекты (коллекции). К ним относятся Структура, Массив, ТаблицаЗначений, Соответствие, а также ссылки на объекты базы данных (например, СправочникСсылка.Номенклатура). Переменная такого типа хранит не сам объект, а лишь указатель (адрес в оперативной памяти), по которому этот объект располагается.

Рассмотрим классический пример ошибочного поведения, с которого часто начинается обсуждение этой темы:


СтруктураШаблон = Новый Структура;
СтруктураШаблон.Вставить("ИмяЭлемента", "Тест");
СтруктураШаблон.Вставить("ВложенныйМассив", Новый Массив);

Результат = Новый Массив;
Для Счетчик = 1 По 3 Цикл
    // Попытка скопировать структуру через конструктор новой структуры
    НовыйЭлемент = Новый Структура(СтруктураШаблон);
    НовыйЭлемент.ИмяЭлемента = "Элемент_" + Строка(Счетчик);
    
    // Добавляем элемент во вложенный массив
    НовыйЭлемент.ВложенныйМассив.Добавить(Счетчик);
    
    Результат.Добавить(НовыйЭлемент);
КонецЦикла;

Казалось бы, мы создаем абсолютно новый объект через конструктор Новый Структура(СтруктураШаблон). Но если мы посмотрим на результат выполнения этого кода, то увидим странную картину: во всех трех элементах массива Результат поле ВложенныйМассив будет содержать абсолютно одинаковый массив из трех элементов: [1, 2, 3].

Это происходит потому, что конструктор Новый Структура выполняет так называемое поверхностное копирование (shallow copy). Он действительно создает новую структуру в памяти, копирует туда ключи и значения из шаблона. Но для поля ВложенныйМассив значением является адрес (указатель) на массив в оперативной памяти. Платформа просто копирует этот адрес. В итоге у нас появляются три разные структуры, но поле ВложенныйМассив в каждой из них ссылается на один и тот же физический массив в памяти. Любое изменение этого массива через любой экземпляр структуры мгновенно отражается во всех остальных копиях.

Разбираем работу ключевого слова Знач

В процессе поиска решения разработчики часто пытаются использовать ключевое слово Знач в параметрах процедур и функций, надеясь, что оно заставит платформу принудительно скопировать объект «по значению». Давайте разберем, как на самом деле ведет себя платформа в этом случае.

Рассмотрим процедуру с передачей параметров по умолчанию (по ссылке на переменную) и с ключевым словом Знач:


Процедура ИзменитьДанные(ПеременнаяПоСсылка, Знач ПеременнаяПоЗначению)
    // Код процедуры
КонецПроцедуры

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


Процедура ВыполнитьПример()
    МоеЧисло = 10;
    МодифицироватьЧисло(МоеЧисло);
    // Здесь значение МоеЧисло станет равным 100!
КонецПроцедуры

Процедура МодифицироватьЧисло(ВходящийПараметр)
    ВходящийПараметр = 100;
КонецПроцедуры

Если же мы используем ключевое слово Знач, платформа создает локальную копию содержимого переменной для использования внутри процедуры. Для примитивных типов это работает идеально: копия числа 10 — это новое независимое число 10. Но что происходит, когда мы передаем сложный объект (например, Структура или Массив) через Знач?

Платформа копирует содержимое переменной. А содержимым переменной сложного объекта является указатель на адрес в памяти! То есть, локальная переменная внутри процедуры получает копию адреса, но этот адрес ведет на тот же самый физический объект в памяти. Поэтому код ниже приведет к изменению исходной структуры, несмотря на использование Знач:


Процедура ВыполнитьПример()
    МояСтруктура = Новый Структура("Ключ", "ИсходноеЗначение");
    МодифицироватьСтруктуру(МояСтруктура);
    // Внимание! МояСтруктура.Ключ теперь содержит "НовоеЗначение"!
КонецПроцедуры

Процедура МодифицироватьСтруктуру(Знач ВходящаяСтруктура)
    ВходящаяСтруктура.Вставить("Ключ", "НовоеЗначение");
КонецПроцедуры

Таким образом, ключевое слово Знач делает лишь поверхностную копию ссылки для объектов и коллекций, но не клонирует сам объект целиком.

Решения проблемы: как выполнить глубокое копирование (Deep Copy)

Для того чтобы разорвать связь между шаблоном и его копиями, нам необходимо выполнить глубокое копирование (deep copy) — то есть создать новые физические экземпляры не только для самой структуры, но и для всех вложенных в нее коллекций (массивов, таблиц значений, других структур). Рассмотрим четыре практических способа реализации этой задачи.

Способ 1. Инициализация через сериализацию в JSON (Самый современный метод)

Если ваша задача изначально связана с подготовкой данных для JSON-выгрузки, этот метод является лучшим выбором. Мы преобразуем структуру со всеми вложенными массивами в строку JSON, а затем читаем её обратно. При десериализации платформа физически заново инициализирует абсолютно все вложенные объекты.

Рассмотрим функцию глубокого копирования через JSON:


Функция ПолучитьГлубокуюКопиюЧерезJSON(ИсходныйОбъект)
    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();
    ЗаписатьJSON(ЗаписьJSON, ИсходныйОбъект);
    СтрокаJSON = ЗаписьJSON.Закрыть();
    
    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
    Возврат ПрочитатьJSON(ЧтениеJSON);
КонецФункции

Применим этот метод в цикле генерации наших структур:


СтруктураШаблон = Новый Структура;
СтруктураШаблон.Вставить("Счетчик", 0);
СтруктураШаблон.Вставить("СчетчикМассив", Новый Массив);

Результат = Новый Массив;
Для Счетчик = 1 По 5 Цикл
    // Создаем полностью независимую глубокую копию
    Врем = ПолучитьГлубокуюКопиюЧерезJSON(СтруктураШаблон);
    
    Врем.Счетчик = Счетчик;
    Врем.СчетчикМассив.Добавить(Счетчик); // Теперь этот массив уникален для каждого шага!
    
    Результат.Добавить(Врем);
КонецЦикла;

Преимущества метода: Работает «из коробки» на любых управляемых формах и сервере, идеально подходит для последующей интеграции с веб-сервисами, гарантирует полную изоляцию данных.

Ограничения: Не подходит для объектов, которые JSON не умеет сериализовать (например, ссылки на справочники 1С, типы данных УникальныйИдентификатор, двоичные данные).

Способ 2. Использование системных функций сериализации во внутреннюю строку

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

Реализуем копирование одной строкой кода:

КопияОбъекта = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(ИсходныйОбъект));

Рассмотрим, как это будет выглядеть в коде инициализации:


СтруктураШаблон = Новый Структура;
СтруктураШаблон.Вставить("НоменклатураСсылка", Справочники.Номенклатура.ПустаяСсылка());
СтруктураШаблон.Вставить("МассивЦен", Новый Массив);

Результат = Новый Массив;
Для Счетчик = 1 По 5 Цикл
    // Быстрое глубокое копирование с поддержкой ссылочных типов 1С
    Врем = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(СтруктураШаблон));
    Врем.МассивЦен.Добавить(Счетчик * 100);
    
    Результат.Добавить(Врем);
КонецЦикла;

Преимущества метода: Максимальная скорость работы на уровне платформы. Поддерживает копирование любых типов данных 1С (ссылки, перечисления, таблицы значений, границы, уникальные идентификаторы).

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

Способ 3. Функция рекурсивного обхода коллекций (Чистый код без сериализации)

Если вы хотите избежать накладных расходов на сериализацию и десериализацию объектов, можно написать функцию ручной инициализации, которая будет рекурсивно обходить все ключи структуры и создавать новые экземпляры объектов в зависимости от их типов.

Давайте реализуем универсальную функцию рекурсивного клонирования структур и массивов:


Функция КлонироватьОбъектРекурсивно(Знач Данные)
    ТипДанных = ТипЗнч(Данные);
    
    Если ТипДанных = Тип("Структура") Тогда
        НоваяСтруктура = Новый Структура;
        Для Каждого КлючИЗначение Из Данные Цикл
            НоваяСтруктура.Вставить(КлючИЗначение.Ключ, КлонироватьОбъектРекурсивно(КлючИЗначение.Значение));
        КонецЦикла;
        Возврат НоваяСтруктура;
        
    ИначеЕсли ТипДанных = Тип("Массив") Тогда
        НовыйМассив = Новый Массив;
        Для Каждого ЭлементИзСписка Из Данные Цикл
            НовыйМассив.Добавить(КлонироватьОбъектРекурсивно(ЭлементИзСписка));
        КонецЦикла;
        Возврат НовыйМассив;
        
    ИначеЕсли ТипДанных = Тип("Соответствие") Тогда
        НовоеСоответствие = Новый Соответствие;
        Для Каждого КлючИЗначение Из Данные Цикл
            НовоеСоответствие.Вставить(КлючИЗначение.Ключ, КлонироватьОбъектРекурсивно(КлючИЗначение.Значение));
        КонецЦикла;
        Возврат НовоеСоответствие;
    КонецЕсли;
    
    // Для примитивных типов и ссылок БД просто возвращаем само значение
    Возврат Данные;
КонецФункции

Этот код самостоятельно анализирует уровень вложенности коллекций и гарантирует, что каждый вложенный массив или структура будут созданы через конструктор Новый.

Способ 4. Использование Библиотеки Стандартных Подсистем (БСП)

Если вы ведете разработку в типовой конфигурации 1С (например, 1С:ERP, 1С:Комплексная автоматизация, 1С:Бухгалтерия или 1С:УНФ), вам не нужно писать собственные велосипеды. В составе БСП уже есть готовая, протестированная на миллионах баз функция глубокого копирования.

Вам достаточно вызвать следующий метод из общего модуля:

КопияОбъекта = ОбщегоНазначения.СкопироватьРекурсивно(ИсходныйОбъект);

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

Философский вопрос: почему у Таблицы Значений есть метод Скопировать(), а у Массива — нет?

Завершая разбор темы, ответим на важный вопрос, который часто задают разработчики: почему структура классов 1С спроектирована так неравномерно? Почему для ТаблицаЗначений мы можем легко вызвать метод Скопировать(), а для Массив такого метода не предусмотрено?

Ответ кроется в назначении и сложности архитектуры этих объектов:

  1. Сложность структуры и метаданных. ТаблицаЗначений — это сложный двумерный объект. У нее есть колонки, типы колонок, индексы, строки. При копировании таблицы разработчику часто требуется гибкость: скопировать только структуру колонок без данных (схему), скопировать только определенные строки по условию или скопировать строго ограниченный набор колонок. Именно поэтому метод Скопировать() у таблицы значений перегружен параметрами: ТаблицаЗначений.Скопировать(Строки, Колонки).
  2. Концепция легковесности массивов. Массив по своей сути является максимально простым одномерным списком (линейным контейнером значений). Он создавался как легковесный объект с минимальными накладными расходами памяти. Добавление сложных методов копирования утяжелило бы интерфейс объекта. Для создания копии массива платформенно предлагается либо быстро обойти его циклом, либо использовать стандартные функции копирования коллекций.

Подведем итоги совместной работы

Мы подробно разобрали особенности управления памятью в 1С при работе с переменными примитивных и ссылочных типов. Теперь мы знаем, что:

← На главную