Изменяемые типы во встроенном языке 1С: почему правка «копии» портит исходные данные
Содержание
Мутабельное значение в 1С — это значение, содержимое которого можно изменить после создания, не создавая новое: Массив, Структура, Соответствие, ТаблицаЗначений, СписокЗначений, объекты справочников и документов. Неизменяемые (иммутабельные) — Строка, Число, Дата, Булево, ссылки на объекты базы и фиксированные коллекции.
Частое заблуждение: «присвоил массив другой переменной — получил копию». Нет. Переменная хранит ссылку на коллекцию, и после Копия = Список обе переменные указывают на один и тот же массив. Правка через любую из них видна через вторую. Отсюда большинство «необъяснимых» багов, связанных с мутабельностью в платформе.
Суть мутабельности: значение против ссылки на него
В переменной встроенного языка лежит не сама коллекция, а указатель на неё. Для примитивов это незаметно: строку или число нельзя поменять «изнутри», любое действие с ними даёт новое значение.
С коллекциями иначе. Метод Добавить(), Вставить() или Удалить() меняет тот самый объект в памяти, на который смотрят все переменные.
Список = Новый Массив;
Список.Добавить(1);
Копия = Список;
Копия.Добавить(2);
Сообщить(Список.Количество()); // 2, а не 1
Со строкой такой эффект невозможен:
Текст = "Счёт";
Другой = Текст;
Другой = Другой + " № 15";
Сообщить(Текст); // «Счёт» — исходная строка не изменилась
Во втором примере Другой + " № 15" создаёт новую строку и кладёт её в переменную Другой. Исходное значение никто не трогал.
💡 Простой тест на мутабельность: есть ли у типа методы, которые меняют его содержимое без присваивания (Добавить, Вставить, Удалить, Очистить, Установить). Если есть — тип изменяемый.
Какие типы в платформе изменяемые, а какие нет
Деление идёт не по «простой/сложный», а по тому, можно ли поменять содержимое экземпляра.
| Группа | Типы | Можно ли изменить содержимое |
|---|---|---|
| Примитивы | Строка, Число, Дата, Булево, Неопределено, Null | Нет |
| Ссылки базы | СправочникСсылка, ДокументСсылка и другие …Ссылка | Нет |
| Служебные значения | Тип, ОписаниеТипов, УникальныйИдентификатор, ХранилищеЗначения | Нет |
| Фиксированные коллекции | ФиксированныйМассив, ФиксированнаяСтруктура, ФиксированноеСоответствие | Нет |
| Универсальные коллекции | Массив, Структура, Соответствие, СписокЗначений, ТаблицаЗначений, ДеревоЗначений | Да |
| Объекты данных | СправочникОбъект, ДокументОбъект, наборы записей регистров | Да |
Ссылка и объект — разные значения
ДокументСсылка неизменяема: это идентификатор записи в базе. Чтобы что-то поправить, вызывают Ссылка.ПолучитьОбъект() и получают ДокументОбъект — изменяемую копию данных в памяти.
Каждый вызов ПолучитьОбъект() возвращает новый экземпляр. Если получить объект дважды и поменять реквизит в первом, второй об этом не узнает, пока первый не записан, а второй не перечитан.
Фиксированные коллекции защищают только верхний уровень
ФиксированныйМассив нельзя дополнить или очистить. Но если внутри лежит обычный Массив или Структура, вложенное значение остаётся изменяемым.
Вложенный = Новый Массив;
Внешний = Новый Массив;
Внешний.Добавить(Вложенный);
Защищённый = Новый ФиксированныйМассив(Внешний);
Защищённый[0].Добавить("сюрприз"); // ошибки нет: изменился вложенный массив
Фиксированная коллекция не делает данные неизменяемыми целиком — только саму коллекцию первого уровня.
Мутабельные и иммутабельные значения: что меняется на практике
Разница видна в трёх местах: присваивание, передача в процедуру и кеширование; отдельно — передача между клиентом и сервером.
| Ситуация | Иммутабельное значение | Мутабельное значение |
|---|---|---|
Б = А, затем правка Б | А не меняется | А меняется вместе с Б |
Передача в процедуру с Знач | Изменения внутри не видны снаружи | Правки содержимого видны снаружи |
| Возврат из модуля с повторным использованием | Безопасно | Вызывающий код может испортить кеш |
Ловушка Знач
Многие считают, что Знач защищает массив от изменений. Это не так. Знач копирует ссылку, а не коллекцию.
Процедура Дополнить(Знач Коллекция)
Коллекция.Добавить("из процедуры"); // это вызывающий код увидит
Коллекция = Новый Массив; // а это — нет
КонецПроцедуры
После вызова Дополнить(МойМассив) в МойМассив появится новый элемент. Пропадёт только переприсваивание переменной внутри процедуры.
Клиент и сервер
При вызове серверной процедуры с клиента значение сериализуется и передаётся по сети. На сервере оказывается уже другая коллекция. Общей памяти между клиентом и сервером нет.
Параметр без Знач после серверного вызова передаётся обратно на клиент. Поэтому у серверных процедур, которые не должны ничего возвращать через параметры, ставят Знач: так данные не гоняются по сети дважды. Это вопрос трафика, а не защиты от изменения.
Где мутабельные значения нужны и как их использовать
Мутабельность — не недостаток. Без неё нельзя собрать коллекцию постепенно, а это основная работа обработок и отчётов.
Накопление результата в цикле
Должники = Новый Массив;
Для Каждого Строка Из ТаблицаРасчетов Цикл
Если Строка.Долг > 0 Тогда
Должники.Добавить(Строка.Контрагент);
КонецЕсли;
КонецЦикла;
Одна коллекция, без пересоздания на каждой итерации. Это быстрее и короче, чем копировать массив при каждом добавлении.
Параметр-«накопитель»
Процедура получает структуру и дописывает в неё результаты. Удобно, когда нужно вернуть несколько значений:
Процедура ПроверитьДокумент(Документ, Результат)
Если Не ЗначениеЗаполнено(Документ.Контрагент) Тогда
Результат.Ошибки.Добавить("Не заполнен контрагент");
КонецЕсли;
КонецПроцедуры
Результат = Новый Структура("Ошибки", Новый Массив);
ПроверитьДокумент(ТекущийДокумент, Результат);
Приём рабочий, если из имени процедуры и параметра понятно, что его будут менять. Неявная правка входного параметра — источник ошибок.
Защищённый результат из кеширующего модуля
Общие модули с признаком «Повторное использование возвращаемых значений» (обычно с суффиксом ПовтИсп) кешируют результат функции. Вызывающий код получает тот же экземпляр, что лежит в кеше.
Если вернуть обычный Массив, любой, кто вызовет Добавить(), испортит данные для всех последующих вызовов в рамках кеша. Поэтому результат фиксируют:
// Общий модуль СкладыПовтИсп, повторное использование: на время сеанса
Функция ДоступныеСклады() Экспорт
Запрос = Новый Запрос(
"ВЫБРАТЬ Склады.Ссылка ИЗ Справочник.Склады КАК Склады
|ГДЕ НЕ Склады.ПометкаУдаления");
Результат = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
Возврат Новый ФиксированныйМассив(Результат);
КонецФункции
Если вызывающему коду нужно дополнить список, он делает свою копию:
Склады = Новый Массив(СкладыПовтИсп.ДоступныеСклады());
Склады.Добавить(ДополнительныйСклад);
Конструктор Новый Массив(ФиксированныйМассив) даёт обычный изменяемый массив. Так же работают Новый Структура(ФиксированнаяСтруктура) и Новый Соответствие(ФиксированноеСоответствие).
Параметры сеанса
Значение параметра сеанса доступно в любом месте конфигурации. Для списков в нём используют фиксированные коллекции: случайная правка в одном модуле не должна менять картину для всего сеанса.
Ошибки, которые даёт изменяемость
Эти баги не падают с исключением. Код отрабатывает, но данные оказываются не теми.
Удаление элементов в цикле «Для Каждого»
Удалили строку — индексы остальных сдвинулись, и следующая строка пропускается. В итоге «удаляются не все нулевые строки».
Надёжный вариант — обратный проход с границей, зафиксированной до цикла:
ВсегоСтрок = Таблица.Количество();
Для Сч = 1 По ВсегоСтрок Цикл
Индекс = ВсегоСтрок - Сч;
Если Таблица[Индекс].Сумма = 0 Тогда
Таблица.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
Индексы идут от последнего к нулевому, удаление строки с большим индексом не сдвигает меньшие. Для таблицы значений есть и второй путь: собрать строки через НайтиСтроки(Новый Структура("Сумма", 0)) и удалить каждую из полученного массива.
«Копия» структуры, которая не копия
Настройки = Новый Структура("Склад, Цены", Склад, Новый Массив);
Копия = Новый Структура;
Для Каждого КлючИЗначение Из Настройки Цикл
Копия.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;
Копия.Цены.Добавить(100); // изменится и Настройки.Цены
Структура скопирована, но массив Цены в обеих один. Это поверхностное копирование.
Одна коллекция в нескольких местах
Классика: один и тот же Массив положили значением в несколько ключей соответствия, рассчитывая потом заполнять их по отдельности. Правка по одному ключу «размножается» на все.
Лечится созданием Новый Массив для каждого ключа, а не одного заранее.
Как писать код с учётом изменяемости
Задача — не избавиться от мутабельных значений, а держать под контролем, кто и где их меняет.
- Возвращайте фиксированные коллекции из кеша и общих точек. Модули
ПовтИсп, параметры сеанса, общие настройки. - Делайте копию на границе. Если функция получает коллекцию и собирается её менять, а вызывающему коду это не нужно, первая строка — копия.
- Глубокая копия — через БСП. В конфигурациях на Библиотеке стандартных подсистем есть
ОбщегоНазначения.СкопироватьРекурсивно(): она копирует вложенные массивы, структуры, соответствия, таблицы и деревья значений. - Таблицу значений копируйте её методом.
Таблица.Скопировать()создаёт новую таблицу со своими строками. Если в ячейках лежат массивы, они останутся общими. - Не храните коллекции в глобальных переменных без нужды. Чем шире область видимости изменяемого значения, тем труднее найти, кто его испортил.
Когда копировать не нужно
Копия стоит памяти и времени. Для таблицы на десятки тысяч строк лишний Скопировать() в цикле заметно тормозит.
Не копируйте, если:
- 🔹 коллекция создана внутри процедуры и наружу не уходит;
- 🔹 процедура явно задумана как изменяющая (накопитель результата);
- 🔹 значение и так иммутабельно — строки, числа, ссылки, фиксированные коллекции.
ФиксированныйМассив на верхнем уровне не спасает вложенные коллекции. Если в кеш возвращается массив структур, фиксируйте и структуры, иначе правка Результат[0].Поле пройдёт без ошибки.
Мутабельность в версиях платформы: что изменилось
Сама семантика «переменная хранит ссылку на коллекцию» в 8.x не менялась. Пример с Копия = Список одинаково работает в любой версии 8.
Менялось окружение, в котором изменяемость начинает мешать.
- 🔸 Обычное приложение (8.0–8.1). Код клиента и сервера чаще жил в одном контексте. Одна коллекция могла «гулять» по формам и модулям, и её правки были видны везде.
- 🔸 Управляемое приложение (8.2 и 8.3). Клиент и сервер разделены. Через серверный вызов коллекция уходит копией, поэтому код, который полагался на общую ссылку, перестаёт работать после переноса в управляемые формы.
- 🔸 Кеширующие общие модули. С ними возврат изменяемой коллекции стал источником ошибок, которые проявляются только во втором и последующих вызовах. Отсюда рекомендация отдавать из таких модулей фиксированные значения.
- 🔸 8.3 и БСП. Типовые конфигурации на БСП дают готовую глубокую копию
СкопироватьРекурсивно(), и самописные рекурсивные функции копирования больше не нужны.
💡 Если переносите старую обработку на управляемые формы и она «перестала видеть изменения», первым делом проверьте, не полагалась ли она на общую ссылку на коллекцию между клиентом и сервером.
Проверьте себя
Частые вопросы
Есть ли в 1С слово «мутабельный» в синтаксис-помощнике?
Почему нельзя сделать массив неизменяемым «на месте»?
Новый ФиксированныйМассив(Массив) — это новое значение, исходный массив остаётся изменяемым.Бывает ли в 1С ловушка с коллекцией в значении параметра по умолчанию?
Истина, Ложь, Неопределено. Коллекцию создают в теле процедуры, поэтому общей «скрытой» коллекции между вызовами не возникает.