Обновить

Комментарии 6

Наконец-то дождался статьи на тему команд в ReactiveUI, а то не так уж и много информации о них можно найти в сети (особенно учитывая куцую документацию). У синхронных Reactive-команд есть особенность, о которой в статье упомянуто, но не слишком широко. Речь про «глотание» ошибок, выброшенных в подписчике. В такой ситуации в приложении не происходит ровным счетом ничего, только кнопка перестает реагировать на команду, так как команда завершилась с ошибкой. Максимум что можно увидеть — скромную строчку Exception thrown: 'System.Exception' in ReactiveUI.Test.exe в логе при запуске с дебагом.
В итоге в подписчике синхронных команд крайне опасно вызывать методы, которые могут выкинуть исключения. Любое проскочившее необработанное исключение просто «убьет» команду, не выдав никакого уведомления пользователю, а именно не скрашив приложение в соответствии с принципом «Fail fast».
А ведь исключение может быть выброшено где угодно, в том числе и в методе, который был вызван в методе, который был вызван в методе ->… -> который был вызван в подписчике. Получается надо либо как параноику обертывать код подписчика в try-catch с самим Exception в catch-блоке чтобы иметь возможность хотя бы уведомить об ошибке, либо использовать синхронные ReactiveCommand для самых-самых простых вешей, которые уж точно не выкинут исключений.
Иногда из-за этой особенности приходилось эмулировать асинхронность при вызове быстрого синхронного кода:

CommandDoSomething = ReactiveCommand.CreateAsynctTask(() => { Код, который может выкинуть коленце });
CommandDoSomething.ThrownExceptions.Subscribe(...логирование либо уведомление...);
CommandDoSomething.Subscribe();


Иначе все возможные дефолтные проверки, такие как ArgumentNullException, IndexOutOfRangeException и прочие, при вызове методов из подписчика оказываются бесполезными.
Спасибо за развернутый комментарий! А вы не могли бы привести пример кода, в котором в ответ на исключение при вызове команда окончательно подыхает? В том примере, который есть в статье, код подписчика в Subscribe() бросает исключение, но повторный вызов Execute() возможен и CanExecute() возвращает true.

Реактивные команды реализованы так, что в команде как в IObservable не предполагается возможность возникновения OnError(), потому что тогда после первой же ошибки команда отваливалась бы и нужно было бы создавать ее снова. Каждый запуск команды внутри должен создавать новый IObservable, результат которого при успехе прилетает через саму команду. Если же что-то упало, то исключение либо проглотится (самый неприятный вариант), либо вылетит при вызове метода запуска команды, либо придет через ThrownExceptions. Сама же команда не должна пострадать.

Как ловить исключения в синхронных командах — хороший вопрос. По идее их просто не должно быть. Вот тут создатель фреймворка говорит, что сложный код и код, который бросает исключения — это не для синхронных команд и Subscribe().
In ReactiveUI, you should never put Interesting™ code inside the Subscribe block — Subscribe is solely to log the result of operations, or to wire up properties to other properties.
По-хорошему стоит использовать асинхронные команды для всего сколько-нибудь сложного и/или потенциально падающего. Для синхронных команд же можно попробовать сделать обертку над ReactiveCommand и реализовать Execute по-своему, чтобы каждый раз не оборачивать код в обработчик исключений, и команды громко падали в случае чего. Но я думаю, что в первую очередь стоит стремиться к тому, чтобы подписчики вообще не могли упасть.
Честно говоря, я несколько озадачен. Я использую ReactiveUI в контексте WPF-a и точно помню что при возникновении исключения в подписчике кнопка переставала вызывать команду. Однако, сейчас попробовал воспроизвести ситуацию, и кнопка продолжает нормально работать (вызывать подписчик с исключением) после первого нажатия. Возможно, я ошибаюсь и что-то не так раньше проверял :)
В любом случае, это тихое «проглатывание» исключения доставляет проблемы, так как часто нужно вызывать быструю (без запросов в сеть/базу данных и проч.) логику и это удобно делать через синхронную команду, без костылей в виде асинхронных команд.
В седьмой версии фреймворка обещают сильно изменить команды: сделать нормальную типизацию для синхронных команд, передача Action'a при создании синхронных команд и т.д. Но вот когда седьмая версия выйдет — хз. Никакой информации о дате релиза я так и не смог найти.
Думаю, скорее раньше было так, как вы описали, но сейчас поведение поменялось )

Да, глотание исключений — это неприятно. Тут стоит разделять две вещи: если в подписчике предполагается возможность возникновения исключения, то от синхронных команд стоит отказаться. Но вот если вдруг при разработке где-то прокосячили и примитивный код типа установки свойств в синхронной команде стал падать с исключением, такое обнаружить будет непросто.

Можно, правда, схитрить и синхронные команды создавать асинхронными внутри. То есть вместо ReactiveCommand.Create() + command.Subscribe(action) вызывать что-то типа своего MySyncReactiveCommand.Create(action), который обернет ваш action в асинхронный вариант команды (получится синтаксис как в reactiveui 7). Оверхед на асинхронность остается, но он весь скрыт внутри и в случае чего не придется чистить все эти костыли во всех местах, где есть синхронные команды.

На релиз седьмой версии в ближайшее время, думаю, надеяться не стоит. Каких-то даже примерных сроков я не нашел, и судя по всему, еще есть приличный объем не сделанных задач.
Или для самых простых команд использовать стандартный ICommand…

In the next episode

Всё ещё ждём! Классная серия статей

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации