Как стать автором
Обновить

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

Спасибо за статью!
З.Ы. Если у вас есть пожелания к анализатору исключений, то свистите, я с удовольствием их добавлю.
Можно добавить еще одну диагностику, проверяющую, что исключение не должно выбрасыватся для async void методов.
Т.е. чтобы избежать подобной ситуации:

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}

public void AsyncVoidExceptionsCannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}
Дельный совет! Но для реализации этого дела нужно решить более общую проблему: нужно выяснить, какие исключения может генерировать метод. А вот это задача весьма непростая. Точнее в общем случае она вообще не решается, поскольку является разновидностью проблемы под названием Halting Problem.

Я думаю, как эту штуку побороть для реальных случаев. Если вразумительное решение будет найдено, то тут можно выдавать массу предупреждений. Например, метод потока (или пула потоков) вызывает метод, который может сгенерить исключение (разновидность предложенного ворнинга); или генерация документации для открытых методов, которая добавит потенциальные исключения в секцию и т.п.

Но, пока что, я не знаю, как подойти к этой задаче в рамках розлина…
Я думал над этой задачей, есть следующие мысли — использовать аннотации для отметки методов, которые не выбрасывают исключения и для методов, которые выбрасывают свои собственные исключения. Если метод не помечен аннотацией то считается, что он выбрасывает базовое исключение. Для системных классов и методов (и сторонних библиотек) использовать дополнительную «базу» аннотаций (конечно, это потребует много ручной работы для наполнения базы этого анализатора).

disclaimer: у меня не очень большой опыт .net и я может быть придумал ерунду.
По идее, эти все вещи можно даже на лету выводить. Вот, народ делает такое для вывода not-nullable аннотаций в IntelliJ IDEA — Вывод NotNull-аннотаций по байткоду Java. Теперь дело за малым, скрестить это дело с Розлином и впихнуть в анализатор!:)
Да, и мысль дельная. Тот же R# именно этот подход используется для анализа nullable/not-nullable типов.
Предположим вам надо сделать File.Open, какие исключения вы будете ловить? Предположим, что у нас MVVM, команда на открытие пришла из VM в Model. То есть, в Model мы делаем File.Open. Какие исключения вы будете ловить и где?
Этот пример относительно простой, поскольку можно посмотреть секцию exceptions документации. Но это не будет работать со сторонним, но не стандартным кодом. Можно попробовать прикрутить анализатор, аналогичный приведенному в предыдущем комментарии по ссылке Вывод NotNull-аннотаций по байткоду Java. Но это будет весьма затратная операция.

Именно по этой причине, данная проблема на 100% не решаема.
поскольку можно посмотреть секцию exceptions документации.

А вы там не найдёте всех возможных исключений. Даже половины. Так что же ловить? :)

Если бы File.Open делал я, то я бы поймал все исключения во ViewModel, вывел сообщение пользователю о том, что беда с открытием файла. И фильтранул на OutOfmemory (это сначала и если это не оно, то вывел бы сообщение просто).
поскольку можно посмотреть секцию exceptions документации.

А вы там не найдёте всех возможных исключений. Даже половины. Так что же ловить? :)
Хабра-дубль, сорри.
ну так я ж и пишу, что в общем случае на уровне статического анализа эта задача не решается.
Про статический анализ понятно. Я хотел обсудить другой вопрос: согласны ли вы с тем, что в таких случаях можно поймать все исключения, профильтровать на АДСКИЕ и просто вывести сообщение об ошибке? Иначе однажды исключение пролетит до самого корня и там нам придётся выводить «Спасите-помогите, критическая ошибка», вместо всё того же сообщения «Ошибка открытия файла». Это первый вопрос.

Можете прокомментировать этот комментарий на схожу тему? Это второй вопрос. Тема крайне интересная и до конца не понятая, как мне кажется.
Вы вопрос задавали не мне, но я отвечу как это вижу я.
Мы тоже делаем MVVM — и я хочу на этапе компиляции или статического анализа быть уверенным, что исключения дальше ViewModel никуда не попадут, а во ViewModel — например, показать сообщение об ошибке.
Это ответ на первый вопрос.

Ответ на второй — для нас приемлемо выбрасывать исключения в случае ошибки — ошибка может быть обнаружена глубоко по дереву вызовов, но исправлена только на самом высоком уровне.
Не совсем понял вас, уточните, пожалуйста.
1. Каким образом вы становитесь уверенными с помощью статического анализа в том, что исключение не пройдёт глубже слоя VM?
2. Что будет, если кто-то добавил новый тип исключения в глубине кода и не предупредил или забыл предупредить, что в системе теперь есть новый тип исключения?
1. Стопроцентной уверенности статический анализ, конечно, не даст.
2. Если на верху ожидается отсутствие исключений — то добавление нового исключения никак на это не повлияет.

Конечно, хочется не статического анализа, а контроля на уровне компилятора, который бы не позволил скомпилировать код, который нарушает контракт.
Что значит «наверху ожидается отсутствие исключений»? То есть, если кто-то добавил новый тип, который выбрасывается в определённых обстоятельствах, то верхний уровень никак не обработает и исключение полетит на самый верх? Если так, чтож, великолепно. Именно когда так люди программируют и получаются, например, такие ситуации:

Включаю Visual Studio, потом Kaspersky Password Manager, что-то в нём делаю, затем тыкаю в редактор исходного кода в VS и, вуаля, она падает. А всё потому что где-то был захвачен некий глобальный хук или что-то подобное, а исключение, которое полезло никто не поймал. В 99%, когда программа падает я вижу, что нет ни одной причины по которой она не могла бы спокойно продолжить работать. Даже если предположить, что сейчас мне некоторая фича недоступна, то зачем гасить всё приложение?
Спасибо, что вы домысливаете за меня. Дальше продолжать беседу я не собираюсь.
Я предположил. Вопрос был такой: «Что значит «наверху ожидается отсутствие исключений»? Если я ошибся — поясните. Я не понимаю, но очень хочу понять, потому что считаю тему очень сложной и до конца непонятной.
> согласны ли вы с тем, что в таких случаях можно поймать все исключения, профильтровать на АДСКИЕ и просто вывести сообщение об ошибке?

Не совсем:)

Более подходящим способом обработки исключений будет такой: команда вью-модели ловит только ожидаемые исключения (IOException), а все остальные просто пробрасывает дальше (возможно логгирует). При этом в приложении добавляется глабальный хендлер, который ловит необработанные исключения, показывает глобальное сообщение об ошибке и там уже решает, крашить приложение или продолжать работать.

Плюс этого подхода в том, что обработка ожидаемых и неожидаемых исключений четко разнесена. В этом случае ваша вью-модель не подавит баги (например, ContractException не должны перехватываться никогда, это баги, аналогично не должны подавляться ArgumentExceptions). При этом решение о поведении при встрече необработанных исключений будет в одном месте (глобальном обработчике, которые есть для Windows Forms и WPF), а не размазаны по всем командам.
Продолжение:
> Иначе однажды исключение пролетит до самого корня и там нам придётся выводить «Спасите-помогите, критическая ошибка», вместо всё того же сообщения «Ошибка открытия файла».

Для этого команда должна перехватывать понятные ей исключения оборачивать их в более высокоуровневые исключение (throw ФайлоОченьНужноеДляЭтогоПриложенияНеНайденоИлиИспорчено), которое затем будет уже перехватываться на верхнем уронве. Но показывать сообщения об ошибках в команде, если они не являются рутами — не хорошо.
> Можете прокомментировать этот комментарий на схожу тему? Это второй вопрос. Тема крайне интересная и до конца не понятая, как мне кажется.

Очень толковый комментарий. Возразить нечего.
Действительно, проблема с исключениями в том, что одна и та же концепция (исключения) несет разную информацию: это могут быть баги (ArgumentException и наследники, NullReferenceException, InvalidOperationException), критические невостановливаемые системные ошибки (Critical System Faults) (OutOfMemoryException, StackOverflowException, ExecutionEngineExcpetion, ThreadAbortException и, возможно, одно-два других), ошибки окружения (environmental faulures или recoverable system failrues: IOException, DBException etc), и логические ошибки (DatabaseRecordNotFound, EmployeeAlreadyExistsException etc).

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

Вот пример. Насколько логично, что контроллер или модель, которые используют DAL будут бросать DbException? не логично, поскольку для пользователя модели или контроллера наличие базы данных вообще спрятано. Это деталь реализации модели или контроллера. Поэтому, когда происходит ошибка в DAL-е, у вызывающего кода есть два варианта: обработать исключение самостоятельно (подавить, вернуть значение по умолнчанию, попробовать трижды и закрыть принудительно приложение), или сообщить вызывающему коду о неудаче, при этом использовать термины, понятные для него (это значит, что нужно бросать PersistentStoreUnavaliable или InfrastructureFailure, завернув во внутрь нового исключения исходное).

В этом случае, модель или контроллер перекладывает ответственность за восстановление на вызывающий код.

При этом что делать моделе/контроллеру, если при вызове DAL-а вылетает что-то отличное от DbExcpetion? Можно ли при этом сказать, что PersistentStoreUnavailable? не совсем! Ведь смысл PersistentStoreUnavailable в том, что персистент стор недоступен сейчас, но будет доступен в будущем. Но если мы хватаем System.Exception, то причина может быть не во временной недоступности хранилища, а в баге в реализации слоя доступа к данным, который никогда не будет исправлен. Поэтому оборачивать что-то, что мы не ожидали в PesistentStoreUnavailable опасно, поскольку мы можем сокрыть ошибки!

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

Поэтому исходный посыл статьи по ссылки верен — никто не умеет нормально обрабатывать ошибки.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории