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