Изменяемые и неизменяемые значения во встроенном языке 1С: как не испортить данные по ссылке
Содержание
Мутабельное значение в 1С — это значение, содержимое которого можно поменять после создания: Массив, Структура, Соответствие, ТаблицаЗначений, объекты справочников и документов. Немутабельное (неизменяемое) — то, что изменить нельзя, можно только заменить новым: Число, Строка, Дата, Булево, ссылки и фиксированные коллекции.
Типичная ситуация: разработчик передал структуру с настройками в процедуру, та «на минутку» дописала в неё служебный ключ — и через полчаса отчёт у бухгалтера строится с чужим отбором. Причина — в типах данных платформы 1С: структура передалась по ссылке, и обе процедуры работали с одним и тем же объектом в памяти.
Чем изменяемый тип отличается от неизменяемого
Переменная во встроенном языке хранит не сам объект, а ссылку на него. Для неизменяемых типов это незаметно: изменить число «на месте» нельзя, любое действие даёт новое значение.
А = 10;
Б = А;
Б = Б + 1; // создано новое число 11, А по-прежнему 10
С изменяемыми типами всё иначе. Присваивание копирует ссылку, а не содержимое:
А = Новый Массив;
Б = А;
Б.Добавить("Склад");
Сообщить(А.Количество()); // 1 — А и Б указывают на один массив
Отсюда простое правило для практики:
- 🔒 немутабельное значение можно смело раздавать по коду: никто его не испортит;
- ✏️ мутабельное значение — общий ресурс: любой, у кого есть ссылка, меняет его для всех.
Строка в 1С тоже неизменяемая. Выражение Текст = Текст + "абв" не дописывает символы в старую строку, а создаёт новую. Это важно для производительности — о ней ниже.
Какие типы данных платформы мутабельные, а какие нет
Ниже — справочная таблица по основным типам 1С 8.3. Принцип для типов, которых здесь нет: если у значения есть методы Добавить, Вставить, Удалить, Очистить или свойства, доступные для записи, — оно изменяемое.
| Группа | Неизменяемые | Изменяемые |
|---|---|---|
| Примитивы | Число, Строка, Дата, Булево, Неопределено, Null | — |
| Коллекции | ФиксированныйМассив, ФиксированнаяСтруктура, ФиксированноеСоответствие | Массив, Структура, Соответствие, СписокЗначений |
| Табличные данные | — | ТаблицаЗначений, ДеревоЗначений |
| Прикладные объекты | СправочникСсылка, ДокументСсылка и другие ссылки, УникальныйИдентификатор | СправочникОбъект, ДокументОбъект, наборы записей регистров |
| Прочее | ОписаниеТипов, ХранилищеЗначения | Запрос, ТабличныйДокумент, ТекстовыйДокумент, данные формы |
Ссылка и объект — разные вещи
Частая путаница: Документ.Ссылка и Документ.ПолучитьОбъект(). Ссылка неизменяема, её можно хранить в кэше, сравнивать, передавать куда угодно.
Объект — изменяемый. Каждый вызов ПолучитьОбъект() читает данные из базы и возвращает новый экземпляр. Два вызова подряд дадут два независимых объекта: правки в одном не видны в другом, пока первый не записан.
Фиксированные коллекции
Фиксированный массив, структура и соответствие — неизменяемые «двойники» обычных коллекций. Создаются конструктором из обычной коллекции:
Настройки = Новый Структура("Организация, Период", Организация, Период);
ФиксНастройки = Новый ФиксированнаяСтруктура(Настройки);
// ФиксНастройки.Вставить(...) — метода нет, изменить нельзя
Обратное преобразование тоже есть: Новый Массив(ФиксированныйМассив), Новый Структура(ФиксированнаяСтруктура), Новый Соответствие(ФиксированноеСоответствие). Так получают изменяемую копию.
Фиксация поверхностная. Если в структуре лежит обычный массив, после Новый ФиксированнаяСтруктура(...) этот массив останется изменяемым. Для глубокой фиксации в конфигурациях на БСП есть функция ОбщегоНазначения.ФиксированныеДанные().
Передача по ссылке, Знач и копирование коллекций
Параметры процедур и функций в 1С по умолчанию передаются по ссылке. Вызываемый код может присвоить параметру новое значение — и переменная в вызывающем коде тоже изменится.
Ключевое слово Знач запрещает только это переприсваивание. Знач не защищает содержимое мутабельного значения: процедура получает копию ссылки на тот же массив и спокойно добавляет в него элементы.
Процедура Дополнить(Знач Список)
Список.Добавить("лишний"); // изменит массив вызывающего кода
Список = Новый Массив; // а это уже нет — меняется только локальная переменная
КонецПроцедуры
Сводка поведения:
| Что передаём | Без Знач | Со Знач |
|---|---|---|
| Число, строка, дата | Присваивание параметру меняет переменную вызывающего | Не меняет |
| Массив, структура, таблица | Видны и правки содержимого, и замена значения | Видны правки содержимого, замена — нет |
Как получить настоящую копию
Если процедура должна работать с данными, не трогая оригинал, сделайте копию явно.
ТаблицаЗначенийиДеревоЗначений— методСкопировать(). У таблицы можно передать отбор строк и список колонок.СписокЗначений— тоже методСкопировать().Массив— метода копирования нет. Создайте новый массив и перенесите элементы циклом или черезНовый Массив(Новый ФиксированныйМассив(Исходный)).СтруктураиСоответствие— новый объект и циклДля Каждого КлючИЗначение Из Исходная.- Вложенные коллекции — рекурсивное копирование. В БСП для этого есть
ОбщегоНазначения.СкопироватьРекурсивно().
Все приёмы из пунктов 1–4 дают поверхностную копию. Если в ячейке таблицы лежит массив, копия и оригинал будут делить этот массив.
Вызов сервера с клиента
Между клиентом и сервером значения не передаются по ссылке в привычном смысле: они сериализуются, и на другой стороне создаётся копия. Объект, изменённый на сервере, сам по себе на клиенте не поменяется.
Однако параметр серверного метода без Знач после вызова передаётся обратно на клиент. Для большой коллекции это лишний трафик. Поэтому в серверных функциях, которые не должны возвращать данные через параметры, ставьте Знач.
Повторное использование возвращаемых значений и мутабельность
Самая опасная точка — общие модули с признаком «Повторное использование возвращаемых значений» (На время вызова или На время сеанса). Платформа кэширует результат функции и при следующем вызове с теми же параметрами отдаёт тот же самый объект.
Если функция кэшируемого модуля вернула обычный Массив или Структуру, первый же вызывающий, который что-то в нём поменяет, испортит кэш для всего сеанса. Ошибка проявится не там, где её внесли, а через десятки вызовов в другой подсистеме.
Правильно — возвращать неизменяемое значение:
// Общий модуль РаботаСНастройкамиПовтИсп, повторное использование: на время сеанса
Функция ВидыОперацийСклада() Экспорт
Результат = Новый Массив;
Результат.Добавить(Перечисления.ВидыОпераций.Поступление);
Результат.Добавить(Перечисления.ВидыОпераций.Перемещение);
Возврат Новый ФиксированныйМассив(Результат);
КонецФункции
Вызывающему коду, которому нужно дополнить список, придётся явно сделать копию: Список = Новый Массив(РаботаСНастройкамиПовтИсп.ВидыОперацийСклада()). Это и есть защита: случайно поменять общий кэш уже нельзя.
Это же требование закреплено в стандартах разработки 1С: функции модулей повторного использования должны возвращать фиксированные коллекции или другие неизменяемые значения.
Параметры сеанса тоже хранят значения как общее состояние. Для коллекций в параметрах сеанса используйте фиксированные типы.
Как изменяемость типов влияет на производительность
Мутабельность — не только про ошибки, но и про скорость. Вот где она чаще всего сказывается.
Передача без копирования — быстро
Передать таблицу на 100 000 строк в процедуру ничего не стоит: передаётся ссылка. Затраты появляются, когда вы начинаете копировать. Вызов Скопировать() у большой таблицы внутри цикла по документам может съесть сотни мегабайт памяти рабочего процесса.
Правило: копируйте только тогда, когда действительно собираетесь менять данные и оригинал нужен нетронутым.
Сборка строк — медленно, если склеивать
Строка неизменяема, поэтому каждая конкатенация создаёт новую строку и копирует в неё всё накопленное. При 10 000 итераций это квадратичный рост работы.
// Медленно
Текст = "";
Для Каждого Стр Из Таблица Цикл
Текст = Текст + Стр.Наименование + Символы.ПС;
КонецЦикла;
// Быстро: собираем части в изменяемый массив и склеиваем один раз
Части = Новый Массив;
Для Каждого Стр Из Таблица Цикл
Части.Добавить(Стр.Наименование);
КонецЦикла;
Текст = СтрСоединить(Части, Символы.ПС);
Функция СтрСоединить появилась в платформе 8.3.6. Для очень больших текстов подходит также ЗаписьXML или ЗаписьJSON в строку.
Фиксация тоже стоит ресурсов
Новый ФиксированныйМассив(Массив) создаёт новую коллекцию и переносит в неё элементы. Для кэша, который строится раз за сеанс, это копейки. Для функции, которую вызывают в цикле по строкам документа, — заметная нагрузка. Фиксируйте на выходе из кэшируемой функции, а не на каждом промежуточном шаге.
Сериализация при клиент-серверных вызовах
Чем больше и «глубже» мутабельная коллекция, тем дороже её передать между клиентом и сервером. Таблица значений целиком на клиент не уходит вовсе — на форме её представляет коллекция данных формы. Передавайте на сервер ссылку или минимальную структуру параметров, а тяжёлые данные собирайте уже там.
Ловушки разработчика: где мутабельность ломает код
Эти ошибки встречаются в отчётах, обработках и обменах снова и снова. Каждую легко узнать по симптому.
Классический пример: одна структура на весь цикл
// Ошибка
Строка = Новый Структура("Номенклатура, Количество");
Результат = Новый Массив;
Для Каждого СтрТЧ Из Объект.Товары Цикл
Строка.Номенклатура = СтрТЧ.Номенклатура;
Строка.Количество = СтрТЧ.Количество;
Результат.Добавить(Строка); // добавляется ссылка на тот же объект
КонецЦикла;
// Правильно
Результат = Новый Массив;
Для Каждого СтрТЧ Из Объект.Товары Цикл
Строка = Новый Структура("Номенклатура, Количество",
СтрТЧ.Номенклатура, СтрТЧ.Количество);
Результат.Добавить(Строка);
КонецЦикла;
В первом варианте после цикла массив содержит N ссылок на одну структуру со значениями последней строки табличной части.
Удаление строк без пропусков
Безопасный обратный цикл по индексу:
Всего = Таблица.Количество();
Для Сч = 1 По Всего Цикл
СтрокаТаблицы = Таблица[Всего - Сч];
Если СтрокаТаблицы.Количество = 0 Тогда
Таблица.Удалить(СтрокаТаблицы);
КонецЕсли;
КонецЦикла;
Граница Всего вычислена до цикла, индекс идёт от последней строки к первой. Удаление текущей строки не сдвигает те, что ещё предстоит проверить.
Сравнение коллекций через «=»
Оператор = для двух массивов или структур сравнивает ссылки, а не содержимое. Два массива с одинаковыми элементами дадут Ложь. Чтобы сравнить содержимое, пройдите по элементам или используйте функции сравнения из БСП, если они есть в вашей конфигурации.
Как писать код, устойчивый к изменяемым данным
Несколько привычек, которые снимают большую часть проблем:
- 📥 Не меняйте входные параметры. Процедура, которая получила структуру, читает её. Если нужен изменённый вариант — создайте новую и верните её.
- 🧊 Отдавайте наружу фиксированное. Всё, что кэшируется или живёт дольше одного вызова, —
ФиксированныйМассив,ФиксированнаяСтруктура,ФиксированноеСоответствие. - 🔁 Создавайте объекты внутри цикла. Структура, заполненная перед циклом, почти всегда ошибка.
- 📏 Копируйте осознанно. Не делайте
Скопировать()«на всякий случай» для таблиц на сотни тысяч строк. - 🏷️ Знач — для серверных методов. Он экономит обратную передачу параметров на клиент, но не защищает содержимое коллекции.