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