Изменяемые коллекции в коде и запросах 1С 8.3: где они ломают логику

· 9 мин

Содержание

Мутабельные значения в 1С — это значения, содержимое которых можно изменить после создания, не создавая новый объект: Массив, Структура, Соответствие, СписокЗначений, ТаблицаЗначений, ДеревоЗначений. Неизменяемые — числа, строки, даты, Булево, ссылки на объекты базы и фиксированные коллекции (ФиксированныйМассив, ФиксированнаяСтруктура, ФиксированноеСоответствие).

Типичная ситуация: программист передал массив складов в параметр запроса, потом в цикле дописал туда ещё один склад «для другого отчёта» — и первый запрос неожиданно вернул лишние строки. Или функция общего модуля с повторным использованием вернула структуру настроек, кто-то в форме поменял в ней поле, и до конца сеанса вся база видит «испорченные» настройки. Обе проблемы — про мутабельность, и обе решаются дисциплиной работы со ссылками.

Мутабельные и неизменяемые типы 1С: где проходит граница

В самом языке запросов мутабельности нет: текст запроса — строка, а результат выполнения — новый объект. Изменяемыми бывают значения встроенного языка, которые вы передаёте в запрос и получаете из него.

Главное правило: переменная с мутабельным значением хранит ссылку на объект в памяти, а не сам объект. Присваивание Б = А не копирует массив — обе переменные смотрят на одну и ту же коллекцию.

ТипМожно изменить содержимоеКак получить «замороженную» версию
Число, Строка, Дата, Булево❌ нет — любое изменение даёт новое значениене требуется
Ссылки (СправочникСсылка.*, ДокументСсылка.*)❌ нет — ссылка не меняется, меняется объект в базене требуется
Массив✅ даНовый ФиксированныйМассив(Массив)
Структура✅ даНовый ФиксированнаяСтруктура(Структура)
Соответствие✅ даНовый ФиксированноеСоответствие(Соответствие)
СписокЗначений, ТаблицаЗначений, ДеревоЗначений✅ дафиксированных аналогов нет — копируйте (Скопировать())
ОписаниеТипов❌ нет — методы возвращают новое описаниене требуется

Ссылка на элемент справочника неизменяема, но объект, полученный через ПолучитьОбъект(), — изменяем. Для запроса это неважно: в параметр передают ссылку, и её значение не «уплывёт».

Пример, который удивляет новичков:

Склады = Новый Массив;
Склады.Добавить(ОсновнойСклад);

КопияСкладов = Склады;           // не копия, а вторая ссылка
КопияСкладов.Добавить(СкладБрака);

Сообщить(Склады.Количество());   // 2, а не 1

Поверхностная и глубокая копия

Копирование коллекции в 1С почти всегда поверхностное. Новый массив получает те же элементы, но если элемент сам является массивом или структурой, копия и оригинал продолжат делить его.

  • 🟢 элементы — числа, строки, ссылки: поверхностной копии достаточно;
  • 🟡 элементы — структуры с настройками: копируйте каждую структуру отдельно;
  • 🔴 вложенность в несколько уровней: пишите рекурсивную функцию или используйте готовую из библиотеки конфигурации.

Для таблицы значений метод Скопировать() даёт новую таблицу, но значения в ячейках переносятся как есть. Ссылки на справочники от этого не страдают, а вложенный Массив в колонке произвольного типа останется общим.

Настоящая копия массива делается циклом или через Новый Массив(Новый ФиксированныйМассив(Склады)) — конструктор массива принимает фиксированный массив и создаёт независимый экземпляр.

Как мутабельные значения попадают в параметры запроса

Параметр в тексте запроса объявляется через амперсанд: &Склады, &Период, &Товары. Значение задаётся методом УстановитьПараметр() или прямо через свойство Параметры, которое само по себе — Структура.

Какие мутабельные значения запрос принимает:

  • 📋 Массив, СписокЗначений, ФиксированныйМассив — для условий В (&Список);
  • 📊 ТаблицаЗначений — как источник данных в конструкции ИЗ &Таблица, обычно с помещением во временную таблицу;
  • 🔗 одну колонку ТаблицаЗначений — тоже для В (&Список), если передать результат ВыгрузитьКолонку().
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Остатки.Номенклатура КАК Номенклатура,
|	Остатки.КоличествоОстаток КАК Остаток
|ИЗ
|	РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад В (&Склады)) КАК Остатки";

Запрос.УстановитьПараметр("Период", КонецДня(ТекущаяДатаСеанса()));
Запрос.УстановитьПараметр("Склады", Склады);

Имя регистра в примере условное — подставьте регистр своей конфигурации.

Когда запрос «видит» изменения коллекции

УстановитьПараметр() кладёт в структуру Параметры ссылку на ваш массив, а не его снимок. Данные читаются в момент вызова Выполнить(). Поэтому:

  1. Установили параметр Склады с одним складом.
  2. До Выполнить() добавили в тот же массив второй склад.
  3. Запрос выполнится по двум складам.

После Выполнить() менять массив безопасно: результат уже получен и от коллекции не зависит.

Если один запрос выполняется в цикле с разными наборами, создавайте новый массив на каждой итерации или очищайте старый (Очистить()) осознанно. Иначе в параметр «протекут» значения прошлой итерации.

Требования к таблице значений в параметре

Для ИЗ &Товары у колонок таблицы должны быть заданы типы через ОписаниеТипов. Колонка без типа приводит к ошибке выполнения запроса.

Товары = Новый ТаблицаЗначений;
Товары.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
Товары.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));

Параметр или литерал в тексте: что выбрать

Значение в запрос можно передать двумя способами: параметром (&Склад) или записать прямо в текст. Для текста в языке запросов есть литералы: строки в кавычках, числа, ДАТАВРЕМЯ(2025, 1, 1), ЗНАЧЕНИЕ(Справочник.Валюты.ПустаяСсылка), ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход).

СпособКогда подходитРиски
Параметр &Имялюбые значения, которые меняются от вызова к вызовузабыли установить параметр — запрос не выполнится
ЗНАЧЕНИЕ(...)предопределённые элементы, значения перечислений, пустые ссылкиработает только с предопределёнными данными
Литерал-строка или числоредкие неизменные константы алгоритмасклейка текста с данными пользователя — путь к поломке запроса
Склейка текста через + со значением переменнойпрактически никогдаразный текст на каждый вызов, проблемы с кавычками и форматом чисел

Не собирайте текст запроса конкатенацией с пользовательским вводом. Строка с кавычкой сломает синтаксис, а дата или число в тексте зависят от формата представления. Параметр снимает обе проблемы.

Если значение одно на всю конфигурацию и не меняется — перечисление или предопределённый элемент через ЗНАЧЕНИЕ() читаются лучше, чем параметр. Всё, что приходит из формы, настроек или другого запроса, — только параметром.

Производительность: параметры, план запроса и большие списки

В клиент-серверном варианте платформа транслирует запрос в SQL для СУБД. Параметры передаются отдельно от текста, поэтому одинаковый текст с разными значениями позволяет СУБД переиспользовать план. Запрос, текст которого склеивается с данными, каждый раз выглядит для СУБД новым.

Что влияет на скорость при работе с мутабельными коллекциями:

  • 📈 размер списка в В (&Список). Десятки значений — нормально. Тысячи — лучше положить во временную таблицу и соединить с ней;
  • 🧮 дубли в массиве. Платформа их не чистит за вас. Перед передачей можно свернуть список: для ТаблицаЗначений — Свернуть(), для массива — пропустить значения через Соответствие;
  • 🔁 запрос в цикле. Сто вызовов с одним значением дольше одного вызова со списком из ста. Соберите значения в массив и выполните запрос один раз;
  • 🏷 составной тип элементов. Если в массиве смешаны ссылки разных справочников, условие сравнивает по составному типу. Держите в списке значения одного типа.

Большой список через временную таблицу

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
|	Т.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Номенклатура
|ИЗ
|	&Товары КАК Т
|ИНДЕКСИРОВАТЬ ПО
|	Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	Цены.Номенклатура КАК Номенклатура,
|	Цены.Цена КАК Цена
|ИЗ
|	РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
|			&Период,
|			Номенклатура В
|				(ВЫБРАТЬ
|					ВТ.Номенклатура
|				ИЗ
|					ВТ_Номенклатура КАК ВТ)) КАК Цены";

Запрос.УстановитьПараметр("Товары", Товары);
Запрос.УстановитьПараметр("Период", ТекущаяДатаСеанса());
Результат = Запрос.Выполнить();

Индекс по временной таблице имеет смысл, когда строк много и по полю идёт соединение или отбор. На десяти строках он только добавит работы.

Временные таблицы — снимок, а не живая ссылка

Здесь мутабельность работает наоборот. Когда таблица значений уходит во временную таблицу через ПОМЕСТИТЬ, данные копируются на сторону СУБД. Дальнейшие изменения исходной ТаблицаЗначений во временную таблицу не попадут.

Порядок, который часто путают:

  1. Поместили &Товары в ВТ_Товары первым запросом.
  2. Добавили в таблицу Товары новую строку.
  3. Выполнили второй запрос, который читает ВТ_Товары.
  4. Новой строки во втором запросе нет — временная таблица уже создана.

Чтобы обновить данные, временную таблицу удаляют конструкцией УНИЧТОЖИТЬ ВТ_Товары и создают заново, либо используют новый менеджер временных таблиц.

Временные таблицы живут, пока жив МенеджерВременныхТаблиц. Менеджер — сам мутабельный объект: передавая его в другие процедуры, вы даёте им право удалять и создавать в нём таблицы. Договоритесь об именах таблиц, чтобы процедуры не затирали данные друг друга.

Фиксированные коллекции и модули повторного использования

Общий модуль с признаком «Повторное использование возвращаемых значений» кэширует результат функции на время вызова или сеанса. Если функция вернула Структура или Массив, вызывающий код получает ссылку на закэшированный объект. Одно изменение — и испорчен кэш для всех последующих вызовов.

Поэтому стандарты разработки 1С требуют возвращать из таких модулей фиксированные значения:

Функция НастройкиУчета() Экспорт

	Настройки = Новый Структура;
	Настройки.Вставить("ВалютаУчета", Константы.ВалютаРегламентированногоУчета.Получить());
	Настройки.Вставить("ИспользоватьСклады", Истина);

	Возврат Новый ФиксированнаяСтруктура(Настройки);

КонецФункции

Имя константы в примере взято для иллюстрации — проверьте его в своей конфигурации.

Нюанс: конструктор фиксированной коллекции «замораживает» только верхний уровень. Если внутри структуры лежит Массив, он останется изменяемым. Вложенные коллекции фиксируйте отдельно. В конфигурациях на БСП для глубокой заморозки есть функция ОбщегоНазначения.ФиксированныеДанные().

Если коду нужно поменять полученные настройки, делайте изменяемую копию:

Настройки = Новый Структура(ОбщийМодульПовтИсп.НастройкиУчета());
Настройки.ИспользоватьСклады = Ложь;   // меняем копию, кэш цел

Передача между клиентом и сервером

При серверном вызове мутабельные значения сериализуются: сервер получает копию, а не ваш объект. Параметры без Знач в серверной процедуре после вызова передаются обратно на клиент. Если возвращать их не нужно, объявляйте параметр с Знач — меньше трафика при каждом вызове.

Ошибки с мутабельными значениями, которые ищут часами

Здесь собраны симптомы, а не повтор правил выше.

Диагностика: выберите симптом

Последний пункт — самый частый источник споров на код-ревью. Знач защищает переменную вызывающего кода от присваивания нового значения, но не защищает содержимое коллекции. Массив.Добавить() внутри процедуры с Знач Массив изменит исходный массив.

Частые вопросы

Можно ли передать в параметр запроса ФиксированныйМассив?
Да. Для условия В (&Список) фиксированный массив работает так же, как обычный, и защищает список от случайных изменений до выполнения запроса.
Является ли результат запроса мутабельным?
РезультатЗапроса нельзя изменить. А вот ТаблицаЗначений, полученная через Выгрузить(), — обычная изменяемая коллекция, независимая от результата.
Как проверить в коде, что значение фиксированное?
Сравните тип: ТипЗнч(Значение) = Тип("ФиксированнаяСтруктура"). Для массива и соответствия — аналогично с типами ФиксированныйМассив и ФиксированноеСоответствие.
Чем Скопировать() у таблицы значений отличается от присваивания?
Присваивание даёт вторую ссылку на ту же таблицу. Скопировать() создаёт новую таблицу с теми же строками, а с параметрами — только нужные строки и колонки.
Есть ли в языке запросов аналог констант алгоритма?
Отдельных объявлений нет. Используйте литералы и ЗНАЧЕНИЕ() для неизменных значений, а всё, что приходит извне, передавайте параметрами.