Почему реквизит типа СтандартныйПериод изменяется на управляемой форме без обратного вызова метода ЗначениеВРеквизитФормы?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с управляемыми формами в платформе 1С:Предприятие 8.3 разработчики часто сталкиваются с поведением контекста, которое на первый взгляд кажется нелогичным или даже ошибочным. Классический сценарий: мы берем данные управляемой формы, конвертируем их в прикладной объект на сервере с помощью метода РеквизитФормыВЗначение, вызываем экспортный метод объекта, внутри которого меняем значения нескольких реквизитов (например, организацию, флаги и период), и завершаем выполнение процедуры. При этом метод ЗначениеВРеквизитФормы мы намеренно не вызываем, ожидая, что данные на форме останутся прежними.

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

Воспроизведение ситуации на тестовом примере

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

В модуле этой обработки определена экспортная процедура, которая выполняет регламентные действия и параллельно перезаполняет эти реквизиты:


Процедура ВыполнитьРегламентнуюОперацию() Экспорт
    
    // Меняем значения реквизитов внутри прикладного объекта
    Организация = Справочники.Организации.ПустаяСсылка();
    Флаг1 = Истина;
    
    // Изменяем свойства периода напрямую
    Период.ДатаНачала = НачалоМесяца(ТекущаяДата());
    Период.ДатаОкончания = КонецМесяца(ТекущаяДата());
    
КонецПроцедуры

На форме обработки по нажатию кнопки вызывается серверная процедура:


&НаСервере
Процедура КомандаВыполнитьНаСервере()
    
    // Получаем прикладной объект из данных формы
    ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
    
    // Выполняем алгоритм
    ОбработкаОбъект.ВыполнитьРегламентнуюОперацию();
    
    // Обратите внимание: метод ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект") НЕ вызывается!
    
КонецПроцедуры

Запустим этот код. После завершения работы процедуры мы увидим, что реквизиты Организация и Флаг1 на форме сохранили свои первоначальные значения. А вот реквизит Период на форме обновился — в нем отобразятся новые даты начала и окончания. На первый взгляд это выглядит как баг платформы, но на самом деле здесь кроется важная архитектурная особенность управления памятью.

Выясняем причину: простые типы против сложных ссылочных типов

Чтобы понять логику платформы, проанализируем разницу в поведении различных типов данных при вызове метода РеквизитФормыВЗначение. Этот метод создает в оперативной памяти сервера новый временный экземпляр прикладного объекта (в нашем случае типа ВнешняяОбработкаОбъект) и заполняет его реквизиты данными, которые пришли с клиента в структуре данных формы.

Процесс заполнения реквизитов созданного объекта разделяется на два принципиально разных механизма в зависимости от типа данных:

  1. Простые типы данных (передача по значению). Такие типы как Строка, Число, Булево и Дата являются примитивными. При конвертации платформой происходит копирование их значений в новую выделенную область памяти для созданного объекта. Изменяя переменную Флаг1 в модуле объекта, мы меняем локальную копию в памяти сервера. Оригинальные данные формы при этом никак не затрагиваются. Чтобы вернуть их назад на форму, нам обязательно требуется вызвать метод ЗначениеВРеквизитФормы.
  2. Сложные типы-объекты (передача по ссылке). Такие типы как СтандартныйПериод, СписокЗначений, Структура или Соответствие представляют собой полноценные системные объекты. Внутри данных управляемой формы хранится не само значение, а ссылка на область оперативной памяти, где расположен этот объект. При вызове РеквизитФормыВЗначение платформа 1С в целях оптимизации производительности и экономии оперативной памяти сервера не производит глубокое копирование (deep clone) таких объектов. Вместо этого она просто копирует ссылку на адрес в памяти. В результате и реквизит формы, и реквизит созданного прикладного объекта начинают ссылаться на один и тот же физический объект в памяти сервера.

Именно из-за этого общего адреса в памяти и происходит так называемое «протекание» контекста.

Мутация свойств против перезаписи ссылки

Теперь рассмотрим, почему поведение реквизита типа СтандартныйПериод может отличаться в зависимости от того, как именно мы пишем код в модуле объекта. Разберем два сценария изменения периода.

Сценарий 1. Мутация свойств (изменение полей существующего объекта)

Рассмотрим код, который мы использовали в нашем примере:


Период.ДатаНачала = НачалоМесяца(ТекущаяДата());
Период.ДатаОкончания = КонецМесяца(ТекущаяДата());

В этом случае мы не создаем новый период, а обращаемся к свойствам существующего объекта по ссылке. Платформа переходит по адресу памяти, общему для формы и объекта, и перезаписывает значения внутри него. Поскольку объект один и тот же, изменения мгновенно становятся видны и на форме, даже без обратного вызова ЗначениеВРеквизитФормы.

Сценарий 2. Полная перезапись ссылки (создание нового объекта)

А теперь заменим код в модуле объекта на следующий:


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

Посмотрим, что произойдет теперь. Конструктор Новый СтандартныйПериод выделяет под объект новую, независимую область памяти. Переменная Период в модуле объекта теперь хранит ссылку на этот новый адрес. Однако реквизит формы все еще продолжает ссылаться на старый адрес памяти! В результате связь между формой и объектом разрывается, и изменения на форме не отобразятся. Поведение станет точно таким же, как и для простых типов данных.

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


// Этот код "протечет" на форму сразу же:
МойСписокРеквизит.Добавить("Новое значение");

То список на форме обновится сам. Но если мы переопределим ссылку:


// Этот код на форме не отразится:
МойСписокРеквизит = Новый СписокЗначений;
МойСписокРеквизит.Добавить("Новое значение");

То форма останется неизменной.

Как правильно решать эту проблему в реальных проектах?

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

Способ 1. Принудительное создание нового экземпляра объекта

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


Процедура ВыполнитьРегламентнуюОперацию() Экспорт
    
    // Создаем копию периода, разрывая связь с формой:
    Период = Новый СтандартныйПериод(Период.ДатаНачала, Период.ДатаОкончания);
    
    // Теперь любые изменения безопасны для формы:
    Период.ДатаНачала = НачалоМесяца(ТекущаяДата());
    Период.ДатаОкончания = КонецМесяца(ТекущаяДата());
    
КонецПроцедуры

Способ 2. Использование бескомтекстных серверных вызовов (Рекомендуемый подход)

Наиболее правильным с точки зрения стандартов разработки 1С является полный отказ от получения тяжелого прикладного объекта через РеквизитФормыВЗначение, если вам нужно просто выполнить расчеты. Перенесите логику в методы без контекста формы или в общие модули, передавая только необходимые параметры в виде простых типов:


&НаКлиенте
Процедура КомандаВыполнить(Команда)
    
    // Вызываем быстрый серверный метод без передачи контекста всей формы
    ВыполнитьРасчетНаСервере(Объект.Организация, Объект.Период.ДатаНачала, Объект.Период.ДатаОкончания);
    
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ВыполнитьРасчетНаСервере(Знач ПереданнаяОрганизация, Знач ДатаНачала, Знач ДатаОкончания)
    
    // Выполняем расчеты с использованием переданных значений простых типов.
    // Это гарантирует, что данные на форме останутся в исходном состоянии.
    РегламентныеАлгоритмы.ВыполнитьОбработкуДанных(ПереданнаяОрганизация, ДатаНачала, ДатаОкончания);
    
КонецПроцедуры

Способ 3. Временное сохранение и восстановление состояния

В случаях, когда логика модуля объекта слишком сложна, жестко завязана на свойства самого объекта и переписать её на бескомтекстные методы затруднительно, мы можем применить классический паттерн "снимок состояния" (Memento) непосредственно в серверной процедуре формы:


&НаСервере
Процедура ВыполнитьСИзоляциейПериодаНаСервере()
    
    // 1. Сохраняем исходное значение периода во временную переменную (происходит копирование ссылки)
    ИсходныйПериод = Объект.Период;
    
    // Создаем копию объекта для работы модуля
    ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
    ОбработкаОбъект.ВыполнитьРегламентнуюОперацию();
    
    // 2. Восстанавливаем сохраненное значение обратно в данные формы,
    // нейтрализуя все побочные эффекты мутации объекта в памяти
    Объект.Период = ИсходныйПериод;
    
КонецПроцедуры

Использование этих несложных правил позволит вам избежать трудноуловимых ошибок при разработке сложных управляемых интерфейсов и сделать поведение ваших обработок предсказуемым и надежным.

← На главную