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