При разработке сложных алгоритмов в среде 1С:Предприятие программисты часто сталкиваются с ситуацией, когда данные в двух разных таблицах значений начинают изменяться синхронно, хотя по логике программы они должны быть независимы. Особенно ярко эта проблема проявляется при работе со сложными структурами данных, когда одна таблица значений вложена в ячейку другой таблицы.
В этой статье мы подробно разберем, почему возникает этот эффект, в чем разница между копированием по ссылке и по значению, а также рассмотрим практические способы создания абсолютно независимых копий объектов в оперативной памяти.
Проанализируем базовую ошибку, с которой часто сталкиваются начинающие разработчики. Рассмотрим простой пример программного кода:
ТЗ_1 = Новый ТаблицаЗначений;
ТЗ_1.Колонки.Добавить("Номенклатура");
// Присваиваем одной переменной значение другой
ТЗ_2 = ТЗ_1;
В данном случае операция ТЗ_2 = ТЗ_1 не создает новую таблицу значений. Переменные в 1С, хранящие объекты ссылочного типа (таблицы значений, структуры, массивы, соответствия), содержат не сами данные, а указатель (адрес) на область памяти, где этот объект физически расположен.
В результате этого присваивания обе переменные — и ТЗ_1, и ТЗ_2 — начинают указывать на один и тот же физический объект в оперативной памяти компьютера. Соответственно, любое изменение структуры или строк через переменную ТЗ_2 мгновенно отразится при обращении к ТЗ_1, ведь это один и тот же объект.
Для создания копии объекта разработчики обычно используют стандартный метод платформы Скопировать():
ТЗ_2 = ТЗ_1.Скопировать();
Этот метод создает в памяти совершенно новый объект типа ТаблицаЗначений, копируя структуру колонок и перенося значения строк. Для простых типов данных (числа, строки, даты, булево) и ссылок на объекты базы данных (например, СправочникСсылка) этот метод работает отлично: значения дублируются и становятся независимыми.
Однако, если колонка исходной таблицы значений имеет тип ТаблицаЗначений, Массив или Структура (вложенные коллекции), метод Скопировать() выполняет так называемое поверхностное копирование (Shallow Copy). Платформа создает новую внешнюю таблицу, но в ячейки новой таблицы она записывает всего лишь ссылки на те же самые вложенные объекты, которые лежали в исходной таблице.
Именно поэтому возникает ситуация, описанная автором темы: внешняя таблица значений скопирована и уникальна, но вложенная в её ячейку таблица «Территории» осталась общей для обоих экземпляров. При изменении данных внутри «Территории» во второй таблице изменения автоматически отображаются и в первой.
Выяснить, указывают ли две переменные на один и тот же физический объект в памяти, очень просто. В платформе 1С для сложных объектов оператор равенства (=) сравнивает именно адреса в памяти. Рассмотрим пример проверки:
// Проверка связи основных таблиц
ТаблицыСвязаны = (ТЗ_1 = ТЗ_2); // Вернет Истина, если это один объект в памяти
// Проверка связи вложенных коллекций в конкретной строке
ВложенныеТаблицыСвязаны = (ТЗ_1[0].Территории = ТЗ_2[0].Территории);
// Вернет Истина, если вложенные таблицы ссылаются на один экземпляр
Этот же прием отлично работает и для объектов типа СтрокаТаблицыЗначений:
СтрокиСвязаны = (ТЗ_1[0] = ТЗ_2[0]);
Для того чтобы полностью разорвать связь между исходной таблицей и её копией, включая все уровни вложенных коллекций, нам необходимо выполнить глубокое копирование. Сделать это можно несколькими способами в зависимости от контекста исполнения кода.
Самый простой и быстрый способ клонировать любой объект «в один клик» — перевести его в системное строковое представление и восстановить обратно. При десериализации платформа физически создаст новые объекты на всех уровнях вложенности:
НоваяТЗ = ЗначениеИзСтрокиВнутр(ЗначениеВСтрокуВнутр(ИсходнаяТЗ));
Преимущества метода: Работает мгновенно, не требует написания сложных циклов обхода, полностью клонирует любую глубину вложенности.
Ограничения: Методы ЗначениеВСтрокуВнутр и ЗначениеИзСтрокиВнутр считаются устаревшими. Они недоступны на тонком и веб-клиенте.
Для использования в управляемом приложении (включая тонкий клиент) отлично подходит универсальная сериализация через механизмы XML. При записи объекта в XML-последовательность и последующем чтении создается абсолютно чистая глубокая копия данных:
Функция ПолучитьГлубокуюКопиюТаблицы(ИсходнаяТаблица) Экспорт
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку();
ЗаписатьXML(ЗаписьXML, ИсходнаяТаблица);
СтрокаXML = ЗаписьXML.Закрыть();
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(СтрокаXML);
Возврат ПрочитатьXML(ЧтениеXML);
КонецФункции
Если в таблице значений содержатся типы данных, которые невозможно сериализовать стандартными средствами, или требуется тонкий контроль процесса копирования, целесообразно написать собственную рекурсивную функцию клонирования:
Функция СкопироватьКоллекциюРекурсивно(ИсходныйОбъект)
ТипОбъекта = ТипЗнч(ИсходныйОбъект);
Если ТипОбъекта = Тип("ТаблицаЗначений") Тогда
КопияТаблицы = ИсходныйОбъект.Скопировать();
Для Каждого СтрокаКопии Из КопияТаблицы Цикл
Для Каждого Колонка Из КопияТаблицы.Колонки Цикл
ИмяКолонки = Колонка.Имя;
ТекущееЗначение = СтрокаКопии[ИмяКолонки];
// Если внутри ячейки лежит коллекция, рекурсивно копируем её
Если ТипЗнч(ТекущееЗначение) = Тип("ТаблицаЗначений")
Или ТипЗнч(ТекущееЗначение) = Тип("Массив")
Или ТипЗнч(ТекущееЗначение) = Тип("Структура") Тогда
СтрокаКопии[ИмяКолонки] = СкопироватьКоллекциюРекурсивно(ТекущееЗначение);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат КопияТаблицы;
ИначеЕсли ТипОбъекта = Тип("Массив") Тогда
КопияМассива = Новый Массив;
Для Каждого Элемент Из ИсходныйОбъект Цикл
КопияМассива.Добавить(СкопироватьКоллекциюРекурсивно(Элемент));
КонецЦикла;
Возврат КопияМассива;
Иначе
// Простые типы возвращаем как есть
Возврат ИсходныйОбъект;
КонецЕсли;
КонецФункции
При изучении типовых конфигураций 1С (таких как «Зарплата и управление персоналом») можно заметить, что разработчики фирмы «1С» активно используют присваивание таблиц значений «напрямую» (например, Зарплата.Начисления = Начисления;). Это делается осознанно по следующим причинам:
При доработке типовых конфигураций всегда учитывайте этот нюанс: если вы получили таблицу значений из типового механизма, никогда не изменяйте её напрямую, предварительно не создав глубокую независимую копию, если эти изменения не должны затронуть связанные бизнес-процессы.