Изменяемые типы во встроенном языке 1С: почему правка «копии» портит исходные данные

· 9 мин

Содержание

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

Частое заблуждение: «присвоил массив другой переменной — получил копию». Нет. Переменная хранит ссылку на коллекцию, и после Копия = Список обе переменные указывают на один и тот же массив. Правка через любую из них видна через вторую. Отсюда большинство «необъяснимых» багов, связанных с мутабельностью в платформе.

Суть мутабельности: значение против ссылки на него

В переменной встроенного языка лежит не сама коллекция, а указатель на неё. Для примитивов это незаметно: строку или число нельзя поменять «изнутри», любое действие с ними даёт новое значение.

С коллекциями иначе. Метод Добавить(), Вставить() или Удалить() меняет тот самый объект в памяти, на который смотрят все переменные.

Список = Новый Массив;
Список.Добавить(1);

Копия = Список;
Копия.Добавить(2);

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

Со строкой такой эффект невозможен:

Текст = "Счёт";
Другой = Текст;
Другой = Другой + " № 15";

Сообщить(Текст); // «Счёт» — исходная строка не изменилась

Во втором примере Другой + " № 15" создаёт новую строку и кладёт её в переменную Другой. Исходное значение никто не трогал.

💡 Простой тест на мутабельность: есть ли у типа методы, которые меняют его содержимое без присваивания (Добавить, Вставить, Удалить, Очистить, Установить). Если есть — тип изменяемый.

Какие типы в платформе изменяемые, а какие нет

Деление идёт не по «простой/сложный», а по тому, можно ли поменять содержимое экземпляра.

ГруппаТипыМожно ли изменить содержимое
ПримитивыСтрока, Число, Дата, Булево, Неопределено, NullНет
Ссылки базыСправочникСсылка, ДокументСсылка и другие …СсылкаНет
Служебные значенияТип, ОписаниеТипов, УникальныйИдентификатор, ХранилищеЗначенияНет
Фиксированные коллекцииФиксированныйМассив, ФиксированнаяСтруктура, ФиксированноеСоответствиеНет
Универсальные коллекцииМассив, Структура, Соответствие, СписокЗначений, ТаблицаЗначений, ДеревоЗначенийДа
Объекты данныхСправочникОбъект, ДокументОбъект, наборы записей регистровДа

Ссылка и объект — разные значения

ДокументСсылка неизменяема: это идентификатор записи в базе. Чтобы что-то поправить, вызывают Ссылка.ПолучитьОбъект() и получают ДокументОбъект — изменяемую копию данных в памяти.

Каждый вызов ПолучитьОбъект() возвращает новый экземпляр. Если получить объект дважды и поменять реквизит в первом, второй об этом не узнает, пока первый не записан, а второй не перечитан.

Фиксированные коллекции защищают только верхний уровень

ФиксированныйМассив нельзя дополнить или очистить. Но если внутри лежит обычный Массив или Структура, вложенное значение остаётся изменяемым.

Вложенный = Новый Массив;
Внешний = Новый Массив;
Внешний.Добавить(Вложенный);

Защищённый = Новый ФиксированныйМассив(Внешний);
Защищённый[0].Добавить("сюрприз"); // ошибки нет: изменился вложенный массив

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

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

Разница видна в трёх местах: присваивание, передача в процедуру и кеширование; отдельно — передача между клиентом и сервером.

СитуацияИммутабельное значениеМутабельное значение
Б = А, затем правка БА не меняетсяА меняется вместе с Б
Передача в процедуру с ЗначИзменения внутри не видны снаружиПравки содержимого видны снаружи
Возврат из модуля с повторным использованиемБезопасноВызывающий код может испортить кеш

Ловушка Знач

Многие считают, что Знач защищает массив от изменений. Это не так. Знач копирует ссылку, а не коллекцию.

Процедура Дополнить(Знач Коллекция)
    Коллекция.Добавить("из процедуры"); // это вызывающий код увидит
    Коллекция = Новый Массив;           // а это — нет
КонецПроцедуры

После вызова Дополнить(МойМассив) в МойМассив появится новый элемент. Пропадёт только переприсваивание переменной внутри процедуры.

Клиент и сервер

При вызове серверной процедуры с клиента значение сериализуется и передаётся по сети. На сервере оказывается уже другая коллекция. Общей памяти между клиентом и сервером нет.

Параметр без Знач после серверного вызова передаётся обратно на клиент. Поэтому у серверных процедур, которые не должны ничего возвращать через параметры, ставят Знач: так данные не гоняются по сети дважды. Это вопрос трафика, а не защиты от изменения.

Где мутабельные значения нужны и как их использовать

Мутабельность — не недостаток. Без неё нельзя собрать коллекцию постепенно, а это основная работа обработок и отчётов.

Накопление результата в цикле

Должники = Новый Массив;
Для Каждого Строка Из ТаблицаРасчетов Цикл
    Если Строка.Долг > 0 Тогда
        Должники.Добавить(Строка.Контрагент);
    КонецЕсли;
КонецЦикла;

Одна коллекция, без пересоздания на каждой итерации. Это быстрее и короче, чем копировать массив при каждом добавлении.

Параметр-«накопитель»

Процедура получает структуру и дописывает в неё результаты. Удобно, когда нужно вернуть несколько значений:

Процедура ПроверитьДокумент(Документ, Результат)
    Если Не ЗначениеЗаполнено(Документ.Контрагент) Тогда
        Результат.Ошибки.Добавить("Не заполнен контрагент");
    КонецЕсли;
КонецПроцедуры

Результат = Новый Структура("Ошибки", Новый Массив);
ПроверитьДокумент(ТекущийДокумент, Результат);

Приём рабочий, если из имени процедуры и параметра понятно, что его будут менять. Неявная правка входного параметра — источник ошибок.

Защищённый результат из кеширующего модуля

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

Если вернуть обычный Массив, любой, кто вызовет Добавить(), испортит данные для всех последующих вызовов в рамках кеша. Поэтому результат фиксируют:

// Общий модуль СкладыПовтИсп, повторное использование: на время сеанса
Функция ДоступныеСклады() Экспорт
    Запрос = Новый Запрос(
        "ВЫБРАТЬ Склады.Ссылка ИЗ Справочник.Склады КАК Склады
        |ГДЕ НЕ Склады.ПометкаУдаления");
    Результат = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
    Возврат Новый ФиксированныйМассив(Результат);
КонецФункции

Если вызывающему коду нужно дополнить список, он делает свою копию:

Склады = Новый Массив(СкладыПовтИсп.ДоступныеСклады());
Склады.Добавить(ДополнительныйСклад);

Конструктор Новый Массив(ФиксированныйМассив) даёт обычный изменяемый массив. Так же работают Новый Структура(ФиксированнаяСтруктура) и Новый Соответствие(ФиксированноеСоответствие).

Параметры сеанса

Значение параметра сеанса доступно в любом месте конфигурации. Для списков в нём используют фиксированные коллекции: случайная правка в одном модуле не должна менять картину для всего сеанса.

Ошибки, которые даёт изменяемость

Эти баги не падают с исключением. Код отрабатывает, но данные оказываются не теми.

Удаление элементов в цикле «Для Каждого»

Удалили строку — индексы остальных сдвинулись, и следующая строка пропускается. В итоге «удаляются не все нулевые строки».

Надёжный вариант — обратный проход с границей, зафиксированной до цикла:

ВсегоСтрок = Таблица.Количество();
Для Сч = 1 По ВсегоСтрок Цикл
    Индекс = ВсегоСтрок - Сч;
    Если Таблица[Индекс].Сумма = 0 Тогда
        Таблица.Удалить(Индекс);
    КонецЕсли;
КонецЦикла;

Индексы идут от последнего к нулевому, удаление строки с большим индексом не сдвигает меньшие. Для таблицы значений есть и второй путь: собрать строки через НайтиСтроки(Новый Структура("Сумма", 0)) и удалить каждую из полученного массива.

«Копия» структуры, которая не копия

Настройки = Новый Структура("Склад, Цены", Склад, Новый Массив);

Копия = Новый Структура;
Для Каждого КлючИЗначение Из Настройки Цикл
    Копия.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;

Копия.Цены.Добавить(100); // изменится и Настройки.Цены

Структура скопирована, но массив Цены в обеих один. Это поверхностное копирование.

Одна коллекция в нескольких местах

Классика: один и тот же Массив положили значением в несколько ключей соответствия, рассчитывая потом заполнять их по отдельности. Правка по одному ключу «размножается» на все.

Лечится созданием Новый Массив для каждого ключа, а не одного заранее.

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

Как писать код с учётом изменяемости

Задача — не избавиться от мутабельных значений, а держать под контролем, кто и где их меняет.

  1. Возвращайте фиксированные коллекции из кеша и общих точек. Модули ПовтИсп, параметры сеанса, общие настройки.
  2. Делайте копию на границе. Если функция получает коллекцию и собирается её менять, а вызывающему коду это не нужно, первая строка — копия.
  3. Глубокая копия — через БСП. В конфигурациях на Библиотеке стандартных подсистем есть ОбщегоНазначения.СкопироватьРекурсивно(): она копирует вложенные массивы, структуры, соответствия, таблицы и деревья значений.
  4. Таблицу значений копируйте её методом. Таблица.Скопировать() создаёт новую таблицу со своими строками. Если в ячейках лежат массивы, они останутся общими.
  5. Не храните коллекции в глобальных переменных без нужды. Чем шире область видимости изменяемого значения, тем труднее найти, кто его испортил.

Когда копировать не нужно

Копия стоит памяти и времени. Для таблицы на десятки тысяч строк лишний Скопировать() в цикле заметно тормозит.

Не копируйте, если:

  • 🔹 коллекция создана внутри процедуры и наружу не уходит;
  • 🔹 процедура явно задумана как изменяющая (накопитель результата);
  • 🔹 значение и так иммутабельно — строки, числа, ссылки, фиксированные коллекции.

ФиксированныйМассив на верхнем уровне не спасает вложенные коллекции. Если в кеш возвращается массив структур, фиксируйте и структуры, иначе правка Результат[0].Поле пройдёт без ошибки.

Мутабельность в версиях платформы: что изменилось

Сама семантика «переменная хранит ссылку на коллекцию» в 8.x не менялась. Пример с Копия = Список одинаково работает в любой версии 8.

Менялось окружение, в котором изменяемость начинает мешать.

  • 🔸 Обычное приложение (8.0–8.1). Код клиента и сервера чаще жил в одном контексте. Одна коллекция могла «гулять» по формам и модулям, и её правки были видны везде.
  • 🔸 Управляемое приложение (8.2 и 8.3). Клиент и сервер разделены. Через серверный вызов коллекция уходит копией, поэтому код, который полагался на общую ссылку, перестаёт работать после переноса в управляемые формы.
  • 🔸 Кеширующие общие модули. С ними возврат изменяемой коллекции стал источником ошибок, которые проявляются только во втором и последующих вызовах. Отсюда рекомендация отдавать из таких модулей фиксированные значения.
  • 🔸 8.3 и БСП. Типовые конфигурации на БСП дают готовую глубокую копию СкопироватьРекурсивно(), и самописные рекурсивные функции копирования больше не нужны.

💡 Если переносите старую обработку на управляемые формы и она «перестала видеть изменения», первым делом проверьте, не полагалась ли она на общую ссылку на коллекцию между клиентом и сервером.

Проверьте себя

Проверьте себя
Что выведет код: А = Новый Массив; Б = А; Б.Добавить(1); Сообщить(А.Количество())?
Защищает ли Знач переданный массив от добавления элементов внутри процедуры?
Можно ли добавить элемент во вложенный Массив, лежащий внутри ФиксированныйМассив?
Какой тип иммутабельный?

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

Есть ли в 1С слово «мутабельный» в синтаксис-помощнике?
Как отдельного свойства типа — нет. Термин используют разработчики и методические материалы. В синтаксис-помощнике изменяемость видна по методам типа и по наличию фиксированного аналога.
Почему нельзя сделать массив неизменяемым «на месте»?
Такого метода нет. Создайте Новый ФиксированныйМассив(Массив) — это новое значение, исходный массив остаётся изменяемым.
Бывает ли в 1С ловушка с коллекцией в значении параметра по умолчанию?
Нет. Значением по умолчанию может быть только литерал: число, строка, дата, Истина, Ложь, Неопределено. Коллекцию создают в теле процедуры, поэтому общей «скрытой» коллекции между вызовами не возникает.
Безопасно ли хранить мутабельное значение в реквизите формы?
Реквизит формы хранится в данных формы и при серверном вызове передаётся копией. Общей ссылки между клиентом и сервером не возникает, но в пределах одной стороны правило то же: присваивание не копирует.
Влияет ли мутабельность на скорость?
Да, в лучшую сторону: изменяемая коллекция дополняется без пересоздания. Тормозит обратное — лишние копии больших таблиц внутри циклов.