Почему при добавлении элементов в массив в 1С изменяются старые значения?

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

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

Суть проблемы: Ссылочная природа объектов

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


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

В данном фрагменте на первый взгляд все верно: внутри цикла создается Новый Структура. Однако проблема часто кроется глубже — в процедурах заполнения. В 1С такие типы данных, как Структура, Массив, ТаблицаЗначений и Соответствие, являются ссылочными. Это означает, что переменная хранит не сами данные, а адрес области памяти, где эти данные лежат.

Когда мы выполняем команду МассивДанныхДляДокументов.Добавить(СтруктураДанных), платформа не копирует содержимое структуры в массив. Она просто записывает в ячейку массива ссылку на тот же самый объект в памяти. Если мы в дальнейшем изменим этот объект (даже через другую переменную или внутри рекурсии), изменения отразятся во всех местах, где хранится эта ссылка.

Разбор рекурсивной ловушки

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


&НаКлиенте
Процедура ЗаполнитьСтруктуруРеквизитаРекурсивно(СтруктураРеквизита, Строка, Лист, ИндексСтроки)
    Если Строка.ТипЗаполнения = "Заполнять указанным значением" тогда 
        СтруктураРеквизита.Значение = Строка.ЗначениеЗаполнения;
    ИначеЕсли Строка.ТипЗаполнения = "Искать по нескольким значениям" тогда 
        Массив = Новый Массив;
        Для Каждого СтруктураДочернегоЭлемента из Строка.ЗначениеЗаполнения Цикл 
            // ОШИБКА ЗДЕСЬ: передается ссылка на элемент коллекции ЗначениеЗаполнения
            ЗаполнитьСтруктуруРеквизитаРекурсивно(СтруктураДочернегоЭлемента, СтруктураДочернегоЭлемента, Лист, ИндексСтроки);
            Массив.Добавить(СтруктураДочернегоЭлемента);
        КонецЦикла;
        СтруктураРеквизита.Значение = Массив;
    КонецЕсли;
КонецПроцедуры

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

Решение №1: Принудительное создание новых объектов

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


МассивДанныхДляДокументов = новый Массив; 
Счетчик = 0; 
Пока Счетчик < 100 Цикл 
    Счетчик = Счетчик + 1; 
    // Сразу добавляем новую структуру в массив
    МассивДанныхДляДокументов.Добавить(Новый Структура); 
    ТекущийИндекс = МассивДанныхДляДокументов.Количество() - 1;
    
    Для Каждого Строка из МассивНастроек Цикл 
        // Создаем новую структуру для каждого реквизита
        НоваяПодструктура = Новый Структура;
        МассивДанныхДляДокументов[ТекущийИндекс].Вставить(Строка.Имя, НоваяПодструктура); 
        
        // Заполняем именно новый объект, а не ссылку на старый
        ЗаполнитьСтруктуруНастроекРекурсивно(МассивДанныхДляДокументов[ТекущийИндекс][Строка.Имя], Строка);
    КонецЦикла; 
КонецЦикла;

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

Решение №2: Глубокое копирование объектов

Иногда нам нужно добавить в массив копию уже существующей структуры. Поскольку простое присваивание копирует только ссылку, нам нужно выполнить так называемое глубокое копирование. Рассмотрим, как это можно сделать.

В 1С нет встроенного метода Клонировать() для структур, поэтому программисты используют обходные пути:

  1. Ручной перебор: создать новую структуру и в цикле Для Каждого КлючИЗначение Из Источник скопировать все свойства. Если внутри есть вложенные коллекции, их тоже нужно перебирать рекурсивно.
  2. Сериализация через JSON или XDTO: это самый надежный способ «разорвать» все связи. Мы превращаем объект в строку и тут же восстанавливаем его обратно.

Проанализируем пример клонирования через JSON:


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

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

Механизм очистки памяти

Часто возникает вопрос: нужно ли явно «уничтожать» структуру после добавления в массив? В языке 1С нет оператора удаления объекта из памяти. Платформа использует автоматический сборщик мусора.

Объект удаляется тогда, когда на него не остается ни одной ссылки. Рассмотрим цепочку событий:

  1. Структура = Новый Структура; — создана ссылка №1 (переменная Структура).
  2. Массив.Добавить(Структура); — создана ссылка №2 (внутри массива).
  3. Структура = Новый Структура; — переменная Структура теперь указывает на другой объект. Ссылка №1 уничтожена, но Ссылка №2 все еще удерживает первый объект в памяти.

Именно поэтому важно переинициализировать переменную Новым объектом ДО того, как вы начнете заполнять данные для следующего элемента массива.

Подведем итоги

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

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

← На главную