Разработка в расширениях 1С — это стандарт современной кастомизации конфигураций. Однако с появлением в платформе 8.3.18 механизмов асинхронного программирования (ключевые слова Асинх и Ждать), разработчики столкнулись с неожиданной проблемой. При попытке совместить аннотацию &Вместо, модификатор Асинх и метод ПродолжитьВызов(), система выдает ошибку выполнения. В этой статье мы подробно разберем, почему это происходит, как устроена внутренняя логика платформы и какие существуют обходные пути.
Рассмотрим ситуацию, с которой сталкивается программист. Необходимо переопределить экспортную процедуру в расширении, добавив в неё асинхронный вызов (например, вывод вопроса пользователю или работу с файловой системой), но при этом сохранив вызов оригинального метода из основной конфигурации. Код выглядит примерно так:
&Вместо("ВыполнитьДействиеЗадачи")
Асинх Процедура расш_ВыполнитьДействиеЗадачи(ДействияЗадач) Экспорт
// Какая-то асинхронная логика
Результат = Ждать ВопросАсинх("Продолжить выполнение?", РежимДиалогаВопрос.ДаНет);
Если Результат = КодВозвратаДиалога.Да Тогда
ПродолжитьВызов(ДействияЗадач);
КонецЕсли;
КонецПроцедуры
При выполнении этого кода платформа генерирует ошибку: «Вызов метода разрешен только в методах с аннотацией "Вместо"». Это сообщение часто вводит в заблуждение, так как аннотация &Вместо на месте. На самом деле причина кроется глубже — в несовместимости механизмов перехвата вызовов и асинхронного контекста.
Проанализируем технические ограничения, которые накладывает платформа 1С на использование асинхронности в расширениях. Основных причин три:
ПродолжитьВызов() работает в рамках единого синхронного стека. Когда выполнение доходит до оператора Ждать, процедура «засыпает», возвращая управление вызывающей среде. В этот момент стек вызовов, связанный с механизмом расширений, фактически прерывается. Платформа 1С на текущем этапе развития не умеет «замораживать» контекст расширения так, чтобы после пробуждения корректно восстановить цепочку вызовов &Вместо -> ПродолжитьВызов().Асинх всегда возвращает объект Обещание (Promise). Если исходный метод в основной конфигурации является синхронным, он не ожидает получить Обещание. Происходит конфликт типов на уровне ядра платформы.ПродолжитьВызов() после завершения ожидания, платформа уже «не помнит», что данный код является частью цепочки расширения. Отсюда и странный текст ошибки о том, что метод разрешен только в &Вместо.Если нам критически важно вызвать ПродолжитьВызов() после интерактивного действия, нам придется отказаться от «синтаксического сахара» Асинх / Ждать и вернуться к использованию ОписаниеОповещения. Это решение выглядит более громоздким, но оно гарантированно работает, так как не меняет сигнатуру метода на асинхронную.
Разберем, как переписать наш пример правильно:
&Вместо("ВыполнитьДействиеЗадачи")
Процедура расш_ВыполнитьДействиеЗадачи(ДействияЗадач) Экспорт
ПараметрыВызова = Новый Структура;
ПараметрыВызова.Вставить("ДействияЗадач", ДействияЗадач);
Оповещение = Новый ОписаниеОповещения("ПослеВопроса", ЭтотОбъект, ПараметрыВызова);
ПоказатьВопрос(Оповещение, "Продолжить выполнение?", РежимДиалогаВопрос.ДаНет);
КонецПроцедуры
&НаКлиенте
Процедура ПослеВопроса(Результат, ДополнительныеПараметры) Экспорт
Если Результат = КодВозвратаДиалога.Да Тогда
// ВНИМАНИЕ: ПродолжитьВызов() здесь вызвать нельзя!
// Это процедура обратного вызова, она вне цепочки расширения.
КонецЕсли;
КонецПроцедуры
Важное уточнение: Как мы видим, даже с ОписаниеОповещения внутри процедуры обратного вызова вызвать ПродолжитьВызов() не получится. Это фундаментальное ограничение: если выполнение метода прервалось (неважно, через Ждать или через ПоказатьВопрос), вернуться в оригинальную цепочку вызовов расширения невозможно.
Проанализируем ситуацию: если мы не можем «продолжить вызов» после ожидания, значит, нам нужно изменить архитектуру вмешательства. Рассмотрим два основных сценария.
Ждать, а затем вызвать основной метод, который уже будет перехвачен расширением синхронно.Ждать. На сервере для схожих задач используется механизм фоновых заданий для выполнения длительных операций.Посмотрим на пример для Сценария Б:
&Вместо("ВыполнитьДействиеЗадачи")
Процедура расш_ВыполнитьДействиеЗадачи(ДействияЗадач) Экспорт
// Запускаем асинхронный процесс без ожидания (Fire and Forget)
РегистрацияСобытияАсинх(ДействияЗадач);
// Продолжаем основной поток выполнения синхронно
ПродолжитьВызов(ДействияЗадач);
КонецПроцедуры
Асинх Процедура РегистрацияСобытияАсинх(Данные)
// Здесь можно использовать Ждать,
// но это не заблокирует основной поток ПродолжитьВызов
Ждать ЗаписатьЛогАсинх(Данные);
КонецПроцедуры
Многие разработчики надеются, что использование &Перед с модификатором Асинх решит проблему. Проанализируем, что произойдет в этом случае. Платформа позволит вам написать такой код, однако основной метод конфигурации не будет ждать завершения вашей асинхронной логики.
Ваша процедура с аннотацией &Перед запустится, дойдет до первого оператора Ждать, вернет управление платформе, и 1С тут же начнет выполнять основной код, не дожидаясь завершения вашего «Обещания». Это может привести к непредсказуемым ошибкам, если основной код зависит от данных, которые должна была подготовить ваша асинхронная процедура.
В ходе обсуждения на форумах часто возникает вопрос: почему 1С выбрала модификатор Асинх перед именем процедуры, а не новую директиву компиляции вроде &НаКлиентеАсинх? Это сделано для того, чтобы разработчик при чтении кода сразу видел характер метода. Асинхронность в 1С — это не свойство места выполнения (клиент или сервер), а свойство самой процедуры, которая меняет свой тип возвращаемого значения на Обещание.
Нужно помнить, что Асинх / Ждать — это не многопоточность. Это синтаксический сахар над механизмом обратных вызовов. Весь код по-прежнему выполняется в одном потоке клиентского приложения. Именно поэтому «засыпание» одной процедуры позволяет платформе выполнять другие задачи, но не позволяет сохранять контекст таких сложных механизмов, как расширение цепочки вызовов.
Подведем итог нашему разбору. Для стабильной работы ваших расширений, в соответствии с принятыми стандартами разработки, придерживайтесь следующих правил:
ПродолжитьВызов() внутри процедур или функций с модификатором Асинх.&Вместо также должно оставаться синхронным.ОписаниеОповещения, но учитывайте, что вызов оригинального кода через ПродолжитьВызов() возможен только ДО вызова немодального окна.Соблюдение этих принципов позволит вам избежать трудноуловимых ошибок времени выполнения и сделает ваш код совместимым с будущими обновлениями платформы 1С:Предприятие. Для превентивного контроля качества кода можно использовать специализированные инструменты для анализа конфигураций и расширений на наличие ошибок.