При разработке на платформе 1С:Предприятие начинающие (а иногда и опытные) разработчики часто сталкиваются с неожиданным поведением программы: при заполнении коллекции (например, строк таблицы значений) сложными объектами в цикле, в итоге все строки оказываются заполнены абсолютно одинаковыми данными — значениями из последней итерации цикла.
В этой статье мы подробно разберем, почему возникает такая ситуация, как устроена работа со ссылочными типами данных в памяти 1С, и рассмотрим несколько практических способов создания независимых копий структур.
Для начала выясним причину такого поведения платформы. В «1С:Предприятии» все сложные типы данных, такие как Структура, Соответствие, Массив, ТаблицаЗначений, передаются и присваиваются по ссылке, а не по значению. Это означает, что когда мы пишем оператор равенства = для структуры, платформа не создает новый физический объект в памяти компьютера. Вместо этого новая переменная просто начинает указывать на ту же самую область памяти, где уже лежит исходный объект.
Проанализируем простейший абстрактный пример:
// Создаем исходную структуру
ИсходнаяСтруктура = Новый Структура("Ключ1", "Значение1");
// Пытаемся скопировать ее простым присваиванием
НоваяСтруктура = ИсходнаяСтруктура;
// Меняем значение в новой структуре
НоваяСтруктура.Ключ1 = "ИзмененноеЗначение";
// Проверяем исходную структуру
Сообщить(ИсходнаяСтруктура.Ключ1); // Выведет "ИзмененноеЗначение"!
Как видим, изменение значения в переменной НоваяСтруктура напрямую затронуло ИсходнаяСтруктура. Они обе ссылаются на один и тот же объект в памяти. Если этот процесс происходит в цикле по строкам таблицы значений, то на каждой итерации мы фактически вставляем в строки одну и ту же ссылку. На последнем шаге цикла мы окончательно перезаписываем свойства этой единственной структуры, из-за чего во всех строках таблицы начинают отображаться данные последней итерации.
Рассмотрим подробнее реальный фрагмент кода из модуля документа выгрузки НДФЛ, где разработчики допустили подобную ошибку:
Если ТаблицаВычетовКДоходам.Количество() > 0 Тогда
ТаблицаДляСтроки = ТаблицаВычетовКДоходам.Скопировать();
ТаблицаДляСтроки.Колонки.КодВычетаСтрокой.Имя = "КодВычет";
ТаблицаДляСтроки.Колонки.СуммаВычета.Имя = "СумВычет";
// В исходную структуру помещается ссылка на новую таблицу
ФорматСведенияОВычетах.Значение = ТаблицаДляСтроки;
// ОШИБКА: СвСумВыч получает структуру, которая хранит ссылку на общий объект ФорматСведенияОВычетах
СтрокаТЗ.СвСумВыч = Новый Структура("СвСумВыч", ФорматСведенияОВычетах);
КонецЕсли;
Поскольку переменная ФорматСведенияОВычетах объявлена вне цикла и на каждой итерации лишь изменяет свойство Значение, все строки СтрокаТЗ в колонке СвСумВыч в итоге получили ссылку на один и тот же экземпляр структуры. Соответственно, при выгрузке отчетов у всех сотрудников дублировались вычеты последнего обработанного человека.
Выясним, какими способами можно разорвать эту связь и передать именно физическую копию данных.
Если структура плоская (не содержит внутри себя других вложенных структур или массивов), самым простым и производительным решением будет ручное создание нового объекта Структура и копирование в него всех ключей в цикле.
Разберем, как выглядит этот алгоритм:
Если ТаблицаВычетовКДоходам.Количество() > 0 Тогда
ТаблицаДляСтроки = ТаблицаВычетовКДоходам.Скопировать();
ТаблицаДляСтроки.Колонки.КодВычетаСтрокой.Имя = "КодВычет";
ТаблицаДляСтроки.Колонки.СуммаВычета.Имя = "СумВычет";
// Создаем абсолютно новый объект структуры в памяти
КопияСтруктуры = Новый Структура;
// Переносим свойства из исходного формата
Для Каждого ЭлементСтруктуры Из ФорматСведенияОВычетах Цикл
КопияСтруктуры.Вставить(ЭлементСтруктуры.Ключ, ЭлементСтруктуры.Значение);
КонецЦикла;
// Заменяем таблицу на ее независимую копию
КопияСтруктуры.Значение = ТаблицаДляСтроки.Скопировать();
// Теперь передается ссылка на уникальный для этой строки объект
СтрокаТЗ.СвСумВыч = Новый Структура("СвСумВыч", КопияСтруктуры);
КонецЕсли;
Важное ограничение метода: это копирование является "поверхностным". Если бы внутри ФорматСведенияОВычетах находилась еще одна структура, то при выполнении строки КопияСтруктуры.Вставить(...) во вложенный ключ записалась бы опять-таки ссылка на вложенный объект, а не его копия.
Если конфигурация разработана на базе Библиотеки стандартных подсистем (БСП) (к таковым относятся ЗУП, ERP, Комплексная автоматизация, УТ, Бухгалтерия предприятия), то нам доступен мощный готовый инструмент для полного рекурсивного клонирования любых коллекций.
Рассмотрим применение метода СкопироватьРекурсивно общего модуля БСП. Этот метод автоматически обходит все вложенные структуры, массивы, соответствия и таблицы значений, создавая их полноценные дубликаты на всю глубину вложенности.
Посмотрим на пример использования:
Если ТаблицаВычетовКДоходам.Количество() > 0 Тогда
ТаблицаДляСтроки = ТаблицаВычетовКДоходам.Скопировать();
ТаблицаДляСтроки.Колонки.КодВычетаСтрокой.Имя = "КодВычет";
ТаблицаДляСтроки.Колонки.СуммаВычета.Имя = "СумВычет";
// Временно удаляем тяжелую таблицу, чтобы не копировать ее дважды
ИсходнаяТаблица = ФорматСведенияОВычетах.Значение;
ФорматСведенияОВычетах.Удалить("Значение");
// Глубокое копирование структуры средствами БСП
СтруктураФормата = ОбщегоНазначения.СкопироватьРекурсивно(ФорматСведенияОВычетах);
// Заполняем индивидуальными таблицами оригинальный объект и его копию
СтруктураФормата.Вставить("Значение", ТаблицаДляСтроки);
ФорматСведенияОВычетах.Вставить("Значение", ИсходнаяТаблица);
СтрокаТЗ.СвСумВыч = Новый Структура("СвСумВыч", СтруктураФормата);
КонецЕсли;
Примечание: В старых версиях БСП данный метод мог называться ОбщегоНазначенияКлиентСервер.СкопироватьСтруктуру(), однако в современных релизах платформы рекомендуется использовать именно рекурсивный метод общего назначения.
Существует крайне элегантный «лайфхак», который позволяет выполнить глубокое копирование объектов без использования БСП и написания сложных рекурсивных циклов. Этот способ основан на преобразовании структуры в формат JSON и её обратной сборке. При десериализации платформа физически вынуждена выделить новые области памяти под каждый элемент.
Рассмотрим реализацию для современных версий платформы 1С (начиная с 8.3.23):
// Копирование в одну строку
КопияСтруктуры = ПрочитатьЗначениеJSON(ЗаписатьЗначениеJSON(ИсходнаяСтруктура));
Для более старых версий платформы, где нет методов работы со значениями напрямую без потоков, мы можем использовать стандартную запись в строку:
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ИсходнаяСтруктура);
СтрокаJSON = ЗаписьJSON.Закрыть();
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
КопияСтруктуры = ПрочитатьJSON(ЧтениеJSON);
Важное ограничение метода: Сериализация в JSON работает только с примитивными типами данных, массивами, соответствиями и структурами. Если ваша структура содержит специфические объекты 1С (такие как ТаблицаЗначений, Запрос, Граница), то при попытке преобразования возникнет ошибка. В нашем примере с вычетами НДФЛ этот способ напрямую не применим, так как внутри структуры лежит объект ТаблицаЗначений, но он идеально подходит для классических сложных деревьев параметров.
Для предотвращения ошибок перезаписи данных при работе со структурами в цикле всегда соблюдайте следующие правила:
= для любых коллекций копирует только ссылку на область памяти.Новый Структура внутри цикла или ручной перебор свойств.ОбщегоНазначения.СкопироватьРекурсивно().