Как ведут себя изменяемые значения во встроенном языке 1С

· 9 мин

Содержание

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

Разница проявляется в одном месте: когда две переменные или процедура и вызывающий код работают с одним и тем же экземпляром. Если тип мутабельный, изменение через одну переменную видно через другую. Если неизменяемый — каждое «изменение» создаёт новое значение, а старое остаётся прежним. Это правило платформы 1С:Предприятие 8.3, от конфигурации оно не зависит.

Изменяемые и неизменяемые значения: в чём суть

Переменная во встроенном языке 1С хранит не сам массив или структуру, а указатель на экземпляр в памяти. Присваивание Б = А копирует указатель, а не содержимое. После этого А и Б смотрят на один объект.

Для неизменяемых типов это ничего не значит. У строки нет метода, который поменял бы её на месте. СтрЗаменить(), ВРег(), Лев() возвращают новую строку. Исходная не трогается.

Для мутабельных типов это значит всё:

  • 🔁 Б.Добавить(5) добавит элемент и в А — это один массив;
  • 🧩 Б.Вставить("Ключ", 1) появится и в исходной структуре;
  • 📄 Б.Удалить(0) удалит строку из таблицы, на которую смотрят обе переменные.

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

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

Б = А;          // одна и та же коллекция
Б.Добавить(2);  // А.Количество() = 2

Б = Новый Массив; // Б теперь новый массив
Б.Добавить(3);    // А не изменился: в нём по-прежнему 1 и 2

Как 1С передаёт значения: ссылка, копия и Знач

В процедуры и функции 1С параметры по умолчанию передаются по ссылке. Это касается любых типов, а не только коллекций. Модификатор Знач переключает передачу на копию переменной.

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

Что меняет Знач, а что нет

Процедура Переназначить(Список)
	Список = Новый Массив; // вызывающий код получит пустой массив
КонецПроцедуры

Процедура ПереназначитьЗнач(Знач Список)
	Список = Новый Массив; // вызывающий код ничего не заметит
КонецПроцедуры

Процедура ДобавитьЗнач(Знач Список)
	Список.Добавить(99); // элемент появится у вызывающего!
КонецПроцедуры
Действие внутри процедурыБез ЗначС Знач
Параметр = Новое значениеменяется переменная вызывающегоне видно снаружи
Параметр.Добавить(...) для массивавидно снаружитоже видно снаружи
Параметр = Параметр + 1 для числаменяется переменная вызывающегоне видно снаружи
Параметр = СтрЗаменить(...) для строкименяется переменная вызывающегоне видно снаружи

Знач не защищает массив, структуру или таблицу значений от изменений. Если процедура не должна портить переданную коллекцию, передавайте копию или фиксированную коллекцию.

Вызов сервера: значения копируются

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

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

Какие типы в 1С мутабельные

Удобно делить типы на три группы.

Изменяемые универсальные коллекции

  • 📦 Массив — Добавить, Вставить, Установить, Удалить, Очистить;
  • 🗂 Структура — Вставить, Удалить, присваивание свойству Структура.Ключ = ...;
  • 🔑 Соответствие — Вставить, Удалить, присваивание по ключу Соотв[Ключ] = ...;
  • 📋 СписокЗначений, ТаблицаЗначений, ДеревоЗначений — добавление и удаление строк, изменение колонок и ячеек.

Объекты прикладных данных

СправочникОбъект, ДокументОбъект, наборы записей регистров, ДанныеФормыСтруктура и коллекции данных формы. Их меняют, а потом записывают в базу методом Записать().

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

Каждый вызов ПолучитьОбъект() создаёт новый экземпляр объекта. Два вызова дадут два независимых объекта: правки в одном не видны в другом до записи.

Неизменяемые значения

  • 🔤 Строка, Число, Дата, Булево, Неопределено, Null;
  • 🔗 ссылки на объекты базы, УникальныйИдентификатор, Тип, ОписаниеТипов;
  • 🔒 ФиксированныйМассив, ФиксированнаяСтруктура, ФиксированноеСоответствие.

У ОписаниеТипов тоже нет методов изменения. Чтобы добавить тип, создают новое описание: Новый ОписаниеТипов(Исходное, "Число").

Примеры изменения коллекций и объектов

Функция, которая «случайно» портит параметр

Типичная ошибка: функция сортирует или фильтрует переданную таблицу, а вызывающий код потом работает с испорченными данными.

Функция СуммаПоПервымТрём(Таблица)
	Таблица.Сортировать("Сумма Убыв"); // меняет исходную таблицу!
	Итог = 0;
	Для Индекс = 0 По Мин(2, Таблица.Количество() - 1) Цикл
		Итог = Итог + Таблица[Индекс].Сумма;
	КонецЦикла;
	Возврат Итог;
КонецФункции

После вызова строки исходной таблицы окажутся отсортированы. Исправление — работать с копией:

	Копия = Таблица.Скопировать();
	Копия.Сортировать("Сумма Убыв");

Удаление элементов в цикле

Массив меняется на месте, поэтому удалять из него элементы прямым циклом нельзя: индексы сдвигаются. Идут с конца:

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

Граница Количество считается один раз до цикла. Удаление элемента с индексом Индекс не сдвигает элементы с меньшими индексами, которые ещё не проверены.

Строка внутри структуры

Неизменяемое значение внутри мутабельной коллекции ведёт себя предсказуемо:

Параметры = Новый Структура("Имя", "Иванов");
Имя = Параметры.Имя;
Имя = Имя + " И.И.";  // новая строка в переменной Имя
// Параметры.Имя по-прежнему "Иванов"
Параметры.Имя = Имя;  // а вот так структура изменилась

Сравнительная таблица поведения типов

ТипМеняется на местеБ = А, затем изменение БКак получить копию
Строка, Число, ДатанетА не меняетсякопия не нужна
Ссылка на справочникнетА не меняетсякопия не нужна
МассивдаА меняетсяНовый Массив(ФиксированныйМассив) или цикл
СтруктурадаА меняетсяцикл Для Каждого с Вставить
СоответствиедаА меняетсяцикл Для Каждого с Вставить
ТаблицаЗначенийдаА меняетсяТаблица.Скопировать()
СправочникОбъектдаА меняетсяновый ПолучитьОбъект() или Скопировать()
ФиксированныйМассивнетизменить нельзяНовый Массив(ФиксированныйМассив)
ФиксированнаяСтруктуранетизменить нельзяНовый Структура(ФиксированнаяСтруктура)

Копирование структуры вручную:

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

Все эти способы дают поверхностную копию. Если в структуре лежит массив, в копию попадёт ссылка на тот же массив. Изменение вложенного массива будет видно в обеих структурах.

Для глубокой копии в конфигурациях на БСП есть функция ОбщегоНазначения.СкопироватьРекурсивно(). Она копирует вложенные массивы, структуры, соответствия, списки и таблицы значений.

Мутабельные значения в кэше, параметрах сеанса и общих модулях

Самые неприятные ошибки с мутабельностью возникают там, где один экземпляр живёт дольше одного вызова.

Модули с повторным использованием возвращаемых значений

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

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

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

Функция КодыСкладов() Экспорт
	Коды = Новый Массив;
	Коды.Добавить("01");
	Коды.Добавить("02");
	Возврат Новый ФиксированныйМассив(Коды);
КонецФункции

Если вызывающему коду нужно что-то добавить, он делает свою копию: Мои = Новый Массив(КодыСкладов()).

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

Параметр сеанса не может иметь тип Массив или Структура. Для набора значений выбирают фиксированные типы, например ФиксированныйМассив или ФиксированнаяСтруктура.

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

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

Фиксация тоже поверхностная

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

Проверьте себя
Процедура объявлена как Процедура Обработать(Знач Список), внутри вызывается Список.Добавить(1). Что увидит вызывающий код?
В переменную Б присвоили А, где А — строка «Иванов». Затем Б = Б + «!». Что в А?
Функция модуля с повторным использованием возвращает Массив, вызывающий код добавил в него элемент. Чем это опасно?

Память и скорость: как работать с изменяемыми и неизменяемыми типами

Строки в цикле

Строка неизменяема, поэтому Текст = Текст + Фрагмент в цикле каждый раз создаёт новую строку и копирует в неё всё накопленное. На тысячах итераций это заметно по времени.

Быстрее собрать фрагменты в мутабельный массив и склеить один раз:

Части = Новый Массив;
Для Каждого Строка Из Таблица Цикл
	Части.Добавить(Строка.Наименование);
КонецЦикла;
Текст = СтрСоединить(Части, Символы.ПС);

СтрСоединить() есть в платформе 8.3.6 и новее. Для больших текстов подходит и ЗаписьXML или ЗаписьТекста с записью в строку или файл.

Практические правила

  1. Не копируйте коллекции «на всякий случай». Копия таблицы на 100 000 строк — это вторая таблица в памяти. Копируйте только там, где функция меняет данные, а вызывающему они нужны в исходном виде.
  2. Если функция меняет параметр намеренно, назовите это в имени: ЗаполнитьКоды(Массив), ДополнитьСтруктуру(Параметры). Функция с именем Получить... параметр менять не должна.
  3. Из модулей с кэшем, параметров сеанса и общих настроек отдавайте фиксированные коллекции.
  4. Для серверных вызовов ставьте Знач у параметров, изменения которых клиенту не нужны: так меньше данных идёт обратно.
  5. Для ключей соответствия используйте неизменяемые значения: строки, числа, ссылки. Мутабельный объект в роли ключа сбивает с толку при поиске.
  6. Объект справочника или документа получайте один раз и передавайте дальше переменной, а не вызывайте ПолучитьОбъект() в каждой процедуре. Иначе правки разойдутся по разным экземплярам.

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

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

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