Передовой опыт обработки исключений в приложении Windows Forms?


118

В настоящее время я пишу свое первое приложение Windows Forms. Я прочитал несколько книг по C #, поэтому у меня есть относительно хорошее представление о том, какие языковые функции C # имеют дело с исключениями. Однако все они носят теоретический характер, поэтому у меня еще нет ощущения, как перевести базовые концепции в хорошую модель обработки исключений в моем приложении.

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

Основные вещи, над которыми я сейчас пытаюсь работать, это:

  • Когда мне следует повторно генерировать исключение?
  • Стоит ли мне попытаться создать какой-то центральный механизм обработки ошибок?
  • Ухудшает ли обработка исключений, которые могут возникнуть, по сравнению с упреждающим тестированием таких вещей, как наличие файла на диске?
  • Следует ли весь исполняемый код заключать в блоки try-catch-finally?
  • Могут ли быть допустимы пустые блоки catch?

Все советы получены с благодарностью!

Ответы:


79

Еще несколько бит ...

У вас обязательно должна быть централизованная политика обработки исключений. Это может быть так же просто, как обертывание Main()в try / catch, быстро завершающееся с изящным сообщением об ошибке для пользователя. Это обработчик исключений «последней инстанции».

Превентивные проверки всегда правильны, если это возможно, но не всегда идеально. Например, между кодом, в котором вы проверяете наличие файла, и следующей строкой, в которой вы его открываете, файл мог быть удален или доступ к нему может быть затруднен какой-либо другой проблемой. Вам все еще нужно попробовать / поймать / наконец в этом мире. При необходимости используйте как превентивную проверку, так и try / catch / finally.

Никогда не «проглатывайте» исключение, за исключением наиболее хорошо задокументированных случаев, когда вы абсолютно уверены в том, что создаваемое исключение пригодно для жизни. Такого почти никогда не будет. (А если это так, убедитесь , что вы глотания только конкретным классом исключения - не когда - нибудь проглотить System.Exception.)

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

throw ex;

Как вы сотрете стек вызовов. Если вам необходимо повторно выбросить (что иногда необходимо, например, при использовании блока обработки исключений корпоративной библиотеки), используйте следующее:

throw;

В конце концов, подавляющее большинство исключений, создаваемых работающим приложением, должно где-то быть раскрыто. Они не должны быть открыты конечным пользователям (поскольку они часто содержат конфиденциальные или другие ценные данные), а обычно должны регистрироваться, а администраторы уведомляются об исключении. Для простоты пользователю может быть представлено стандартное диалоговое окно, возможно, со ссылочным номером.

Обработка исключений в .NET - это больше искусство, чем наука. У каждого будут свои фавориты, которыми можно поделиться здесь. Это всего лишь несколько советов, которые я усвоил с помощью .NET с первого дня, методы, которые не раз спасали мой бекон. Ваш пробег может отличаться.


1
Если позволить исключениям всплывать, как вызывающий должен знать, указывает ли исключение, что операция завершилась неудачно, но система в основном в порядке (например, пользователь попытался открыть поврежденный файл документа; файл не загружается, но все иначе все должно быть в порядке), или это означает, что процессор горит и нужно как можно быстрее направиться к выходу? Что-то вроде ArgumentException может указывать на то и другое, в зависимости от обстоятельств, при которых оно возникло.
supercat

2
@supercat Путем написания конкретных подклассов ApplicationExceptionдля тех случаев отказа, которые приложение должно разумно различать и обрабатывать.
Мэтт Энрайт

@Matt Enright: Конечно, можно поймать собственные исключения, но я не знаю ничего отдаленно напоминающего соглашение, согласно которому модули, генерирующие исключения, указывают, указывают ли они на повреждение какого-либо состояния за пределами того, что подразумевается отказом. В идеале такой метод, как SuperDocument.CreateFromFile (), будет либо успешным, либо вызовет исключение CleanFailure, либо вызовет SomethingReallyBadHappenedException. К сожалению, если не обернуть все, что может вызвать исключение, в его собственный блок catch, невозможно узнать, возникло ли исключение InvalidOperationException ...
supercat

@Matt Enright: ... должно быть заключено в исключение CleanFailureException или SomethingReallyBadHappenedException. А объединение всего в отдельные блоки try-catch, в первую очередь, лишило бы цели наличия исключений.
supercat

2
Я думаю, что это плохой дизайн библиотеки, чтобы скрыть режимы отказа с неточным типом исключения. Не создавайте FileNotFoundException, если на самом деле вы имеете в виду IOException или InvalidDataException, поскольку приложение должно по-разному реагировать на каждый случай. Такие вещи, как StackOverflowException или OutOfMemoryException, не могут разумно обрабатываться приложением, поэтому просто позвольте им пузыриться, поскольку у хорошо работающего приложения есть централизованный обработчик «последней инстанции» в любом случае.
Мэтт Энрайт

63

Есть отличный код статья CodeProject . Вот пара основных моментов:

  • Планируйте худшее *
  • Проверить это заранее
  • Не доверяйте внешним данным
  • Единственные надежные устройства: видео, мышь и клавиатура.
  • Пишет тоже может не получиться
  • Код безопасно
  • Не генерировать новое исключение ()
  • Не помещайте важную информацию об исключении в поле сообщения
  • Поместите один улов (Exception ex) для каждого потока
  • Обнаруженные общие исключения должны быть опубликованы
  • Журнал Exception.ToString (); никогда не записывать только Exception.Message!
  • Не ловить (исключение) более одного раза в потоке
  • Никогда не глотайте исключения
  • Код очистки должен быть помещен в блоки finally
  • Используйте везде
  • Не возвращать специальные значения при возникновении ошибки
  • Не используйте исключения для обозначения отсутствия ресурса
  • Не используйте обработку исключений как средство возврата информации из метода
  • Используйте исключения для ошибок, которые нельзя игнорировать
  • Не очищать трассировку стека при повторной генерации исключения
  • Избегайте изменения исключений без добавления семантической ценности
  • Исключения должны быть помечены [сериализуемые]
  • Если есть сомнения, не утверждать, выбросить исключение
  • Каждый класс исключения должен иметь как минимум три исходных конструктора.
  • Будьте осторожны при использовании события AppDomain.UnhandledException
  • Не изобретайте велосипед
  • Не используйте неструктурированную обработку ошибок (VB.Net)

3
Не могли бы вы подробнее объяснить мне этот момент? «Не используйте исключения для обозначения отсутствия ресурса» Я не совсем уверен, что понимаю причину этого. Также просто замечание: этот ответ вообще не объясняет «почему». Я знаю, что ему 5 лет, но это все еще меня немного
беспокоит

Срок действия ссылки на указанную статью истек. Code Project предполагает, что статья могла быть перемещена сюда .
DavidRR

15

Обратите внимание, что в Windows Forms есть собственный механизм обработки исключений. Если кнопка в форме нажата и ее обработчик генерирует исключение, которое не было перехвачено обработчиком, Windows Forms отобразит свой собственный диалог необработанного исключения.

Чтобы предотвратить отображение диалогового окна необработанных исключений и перехватить такие исключения для ведения журнала и / или для предоставления собственного диалогового окна ошибок, вы можете присоединиться к событию Application.ThreadException перед вызовом Application.Run () в вашем методе Main ().


Спасибо за это предложение, я узнал об этом слишком поздно, я только что проверил его в linqpad, работает, как ожидалось, делегат: void Form1_UIThreadException (отправитель объекта, ThreadExceptionEventArgs t) Еще один хороший источник по этой теме - richnewman.wordpress.com/2007/ 04/08 /… Для общей обработки необработанных исключений: событие AppDomain.UnhandledException предназначено для необработанных исключений, созданных не из основного потока пользовательского интерфейса.
zhaorufei 05

14

Все советы, опубликованные здесь, хороши и заслуживают внимания.

Одна вещь, которую я хотел бы расширить, - это ваш вопрос: «Ухудшает ли обработка исключений, которые могут возникнуть, по сравнению с упреждающим тестированием таких вещей, как наличие файла на диске?»

Наивное эмпирическое правило гласит: «Блоки попыток / уловок стоят дорого». На самом деле это не так. Пробовать не дорого. Это ловушка, когда система должна создать объект Exception и загрузить его трассировкой стека, что дорого. Есть много случаев, когда исключение является достаточно исключительным, чтобы обернуть код в блок try / catch.

Например, если вы заполняете Словарь, это:

try
{
   dict.Add(key, value);
}
catch(KeyException)
{
}

часто быстрее, чем это делать:

if (!dict.ContainsKey(key))
{
   dict.Add(key, value);
}

для каждого отдельного элемента, который вы добавляете, потому что исключение возникает только тогда, когда вы добавляете повторяющийся ключ. (Это делают агрегированные запросы LINQ.)

В приведенном вами примере я бы использовал try / catch почти не задумываясь. Во-первых, тот факт, что файл существует, когда вы его проверяете, не означает, что он будет существовать, когда вы его откроете, поэтому вам действительно следует обрабатывать исключение в любом случае.

Во-вторых, и я думаю, что это более важно, если только вы: а) ваш процесс не открывает тысячи файлов и б) вероятность того, что файл, который он пытается открыть, не существует, нетривиально низка, снижение производительности при создании исключения - это не то, что вы ' когда-нибудь заметит. Вообще говоря, когда ваша программа пытается открыть файл, она пытается открыть только один файл. Это тот случай, когда написание более безопасного кода почти наверняка будет лучше, чем написание максимально быстрого кода.


3
в случае с dict вы можете просто сделать: dict[key] = valueчто должно быть так же быстро, если не быстрее ..
nawfal

9

Вот несколько рекомендаций, которым я следую

  1. Fail-Fast: это больше похоже на руководство по созданию исключений. Для каждого сделанного вами предположения и каждого параметра, который вы вводите в функцию, выполните проверку, чтобы убедиться, что вы начинаете с правильных данных и что ваши предположения Делаем верны. Типичные проверки включают: аргумент не равен нулю, аргумент находится в ожидаемом диапазоне и т. Д.

  2. При повторном выбросе сохранить трассировку стека - это просто переводится на использование throw при повторном выбросе вместо throw new Exception (). В качестве альтернативы, если вы чувствуете, что можете добавить дополнительную информацию, оберните исходное исключение как внутреннее исключение. Но если вы поймаете исключение только для того, чтобы зарегистрировать его, обязательно используйте throw;

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

  4. Перехватите глобальные обработчики исключений и убедитесь, что в журнал занесено как можно больше информации. Для winforms перехватывают события необработанного исключения домена приложения и потока.

  5. Производительность следует принимать во внимание только тогда, когда вы проанализировали код и увидели, что он вызывает узкое место в производительности, по умолчанию оптимизируйте его для удобочитаемости и дизайна. Итак, что касается вашего первоначального вопроса о проверке существования файла, я бы сказал, что это зависит от того, если вы можете что-то сделать с файлом, который не существует, тогда да, сделайте эту проверку, иначе, если все, что вы собираетесь сделать, это выбросить исключение, если файл не там тогда я не вижу в этом смысла.

  6. Определенно бывают случаи, когда требуются пустые блоки catch, я думаю, что люди, которые говорят иначе, не работали над кодовыми базами, которые развивались в течение нескольких выпусков. Но их следует прокомментировать и просмотреть, чтобы убедиться, что они действительно нужны. Наиболее типичный пример - разработчики используют try / catch для преобразования строки в целое число вместо использования ParseInt ().

  7. Если вы ожидаете, что вызывающий ваш код сможет обрабатывать условия ошибки, создайте пользовательские исключения, которые подробно описывают непредвиденную ситуацию и предоставляют соответствующую информацию. В противном случае просто придерживайтесь встроенных типов исключений, насколько это возможно.


Какая польза от подключения обработчиков необработанных исключений домена приложения и потока? Если вы просто используете Appdomain.UnhandledException, разве это не самый общий вариант, который уловит все?
Quagmire

Вы имели в виду обработать Application.ThreadExceptionсобытие, когда ссылались на событие «необработанное исключение потока»?
Jeff B

4

Мне нравится философия не ловить то, что я не собираюсь обрабатывать, что бы это ни значило в моем конкретном контексте.

Я ненавижу, когда вижу такой код, как:

try
{
   // some stuff is done here
}
catch
{
}

Я наблюдал это время от времени, и довольно сложно найти проблемы, когда кто-то «ест» исключения. Мой коллега делает это, и это, как правило, вносит свой вклад в постоянный поток проблем.

Я повторно выбрасываю, если есть что-то, что мой конкретный класс должен сделать в ответ на исключение, но проблема должна быть устранена до того, как вызывается метод, в котором это произошло.

Я считаю, что код следует писать проактивно и что исключения должны быть для исключительных ситуаций, а не для избежания тестирования условий.


@Kiquenet Извините, я немного не понял. Что я имел в виду: не делайте таких пустых уловок, вместо этого добавьте обработчик в Dispatcher.UnhandledException и добавьте хотя бы журнал, чтобы его не ели без хлебных крошек :) Но если у вас нет требования к тихому компоненту, вы должны всегда включать исключения в ваш дизайн. Бросьте их, когда их нужно бросить, сделайте четкие контракты для вас Интерфейсы / API / любой класс.
LuckyLikey

4

Я сейчас ухожу, но расскажу вам, где использовать обработку исключений. Я постараюсь ответить на другие ваши вопросы, когда вернусь :)

  1. Явно проверьте наличие всех известных ошибок *
  2. Добавьте попытку / уловить код, если вы не уверены, смогли ли вы обработать все случаи
  3. Добавьте попытку / уловить код, если вызываемый интерфейс .NET выдает исключение.
  4. Добавьте попытку / уловить код, если он превышает для вас порог сложности
  5. Добавьте попытку / уловить код, если для проверки работоспособности: вы утверждаете, что ЭТО НЕ ДОЛЖНО НИКОГДА ПРОИСХОДИТ
  6. Как правило, я не использую исключения вместо кодов возврата. Это нормально для .NET, но не для меня. Однако у меня есть исключения (хе-хе) из этого правила, это также зависит от архитектуры приложения, над которым вы работаете.

*В пределах разумного. Нет необходимости проверять, попал ли, скажем, космический луч в ваши данные, заставив пару битов перевернуться. Понимание того, что «разумно» - это приобретенный навык для инженера. Трудно дать количественную оценку, но легко интуитивно. То есть я могу легко объяснить, почему я использую try / catch в каком-то конкретном случае, но мне трудно наделить других такими же знаниями.

Я, например, стараюсь избегать архитектур, в значительной степени основанных на исключениях. Команда try / catch не влияет на производительность как таковая, она возникает, когда генерируется исключение, и код может пройти несколько уровней стека вызовов, прежде чем что-либо обработает его.


4

Золотое правило, которого пытались придерживаться, - обрабатывать исключение как можно ближе к источнику.

Если вы должны повторно вызвать исключение, попробуйте добавить к нему, повторное создание исключения FileNotFoundException не очень помогает, но выброс исключения ConfigurationFileNotFoundException позволит захватить его и обработать где-нибудь в цепочке.

Еще одно правило, которому я стараюсь следовать, - не использовать try / catch как форму потока программы, поэтому я проверяю файлы / соединения, проверяю, были ли инициированы объекты и т. Д., Перед их использованием. Try / catch должен быть для исключений, вещей, которые вы не можете контролировать.

Что касается пустого блока catch, если вы делаете что-либо важное в коде, который сгенерировал исключение, вы должны как минимум повторно выбросить исключение. Если код, который вызвал исключение, не имеет последствий, то почему вы его вообще написали.


Это «золотое правило» в лучшем случае произвольно.
Эван Харпер

3

вы можете перехватить событие ThreadException.

  1. Выберите проект приложения Windows в обозревателе решений.

  2. Откройте созданный файл Program.cs, дважды щелкнув по нему.

  3. Добавьте следующую строку кода в начало файла кода:

    using System.Threading;
  4. В методе Main () добавьте следующую строку в качестве первой строки метода:

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
  5. Добавьте следующее под методом Main ():

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        // Do logging or whatever here
        Application.Exit();
    }
  6. Добавьте код для обработки необработанного исключения в обработчике событий. Любое исключение, которое не обрабатывается где-либо еще в приложении, обрабатывается приведенным выше кодом. Чаще всего этот код должен регистрировать ошибку и отображать сообщение для пользователя.

ссылка: https://blogs.msmvps.com/deborahk/global-exception-handler-winforms/


@Kiquenet, извините, я не знаю насчет VB
Omid-RH

2

Исключения дороги, но необходимы. Вам не нужно заключать все в try catch, но вам нужно убедиться, что исключения всегда в конечном итоге перехватываются. Во многом это будет зависеть от вашего дизайна.

Не выполняйте повторный вызов, если можно позволить возникновению исключения. Никогда не позволяйте ошибкам оставаться незамеченными.

пример:

void Main()
{
  try {
    DoStuff();
  }
  catch(Exception ex) {
    LogStuff(ex.ToString());
  }

void DoStuff() {
... Stuff ...
}

Если DoStuff пойдет не так, вы все равно захотите выручить его. Исключение будет передано в main, и вы увидите последовательность событий в трассировке стека ex.


1

Когда мне следует повторно генерировать исключение?

Везде, но методы конечного пользователя ... например, обработчики нажатия кнопки

Стоит ли мне попытаться создать какой-то центральный механизм обработки ошибок?

Я пишу файл журнала ... довольно просто для приложения WinForm

Ухудшает ли обработка исключений, которые могут возникнуть, по сравнению с упреждающим тестированием таких вещей, как наличие файла на диске?

Я не уверен в этом, но я считаю, что это хорошая практика для показа исключений ... Я имею в виду, что вы можете спросить, существует ли файл и не вызывает ли он исключение FileNotFoundException

Следует ли весь исполняемый код заключать в блоки try-catch-finally?

Yeap

Могут ли быть допустимы пустые блоки catch?

Да, допустим, вы хотите показать дату, но не знаете, как эта дата была сохранена (дд / мм / гггг, мм / дд / гггг и т. Д.), Вы пытаетесь разобрать ее, но если это не удается, просто продолжайте .. . если это не имеет отношения к вам ... я бы сказал да, есть


1

Одна вещь, которую я очень быстро усвоил, - это заключать абсолютно каждый фрагмент кода, который взаимодействует с чем-либо вне потока моей программы (например, файловая система, вызовы базы данных, пользовательский ввод), с блоками try-catch. Try-catch может снизить производительность, но обычно в этих местах кода это не заметно и окупается безопасностью.

Я использовал пустые блоки-уловки в местах, где пользователь мог бы сделать что-то, что на самом деле не "неправильно", но может вызвать исключение ... на ум приходит пример в GridView, если пользователь DoubleCLicks серый заполнитель ячейка в верхнем левом углу, она вызовет событие CellDoubleClick, но ячейка не принадлежит строке. В этом случае вы не действительно необходимо отправить сообщение, но если вы его не поймаете, пользователю будет выдана ошибка необработанного исключения.


1

При повторной генерации исключения ключевое слово выбрасывается самим собой. Это вызовет перехваченное исключение и по-прежнему сможет использовать трассировку стека, чтобы узнать, откуда оно взялось.

Try
{
int a = 10 / 0;
}
catch(exception e){
//error logging
throw;
}

это приведет к тому, что трассировка стека завершится оператором catch. (избегайте этого)

catch(Exception e)
// logging
throw e;
}

применимо ли это к самым последним версиям .NET Framework и .NET Core?
LuckyLikey

1

По моему опыту, я счел нужным отлавливать исключения, когда знаю, что собираюсь их создавать. В случаях, когда я использую веб-приложение и выполняю Response.Redirect, я знаю, что получу исключение System.ThreadAbortException. Так как это сделано намеренно, у меня есть ловушка для определенного типа и я просто проглатываю ее.

try
{
/*Doing stuff that may cause an exception*/
Response.Redirect("http:\\www.somewhereelse.com");
}
catch (ThreadAbortException tex){/*Ignore*/}
catch (Exception ex){/*HandleException*/}

1

Я полностью согласен с правилом:

  • Никогда не позволяйте ошибкам оставаться незамеченными.

Причина в том, что:

  • Когда вы впервые записываете код, скорее всего, вы не будете полностью осведомлены о стороннем коде, библиотеке .NET FCL или последних вкладах ваших коллег. В действительности вы не можете отказаться от написания кода, пока не будете хорошо знать все возможные исключения. Так
  • Я постоянно нахожу, что использую try / catch (Exception ex) только потому, что хочу защитить себя от неизвестных вещей, и, как вы заметили, я ловлю Exception, а не более конкретное, например OutOfMemoryException и т.д. И я всегда делаю исключение выскакивает мне (или QA) ForceAssert.AlwaysAssert (false, ex.ToString ());

ForceAssert.AlwaysAssert - это мой личный способ Trace.Assert, независимо от того, определен ли макрос DEBUG / TRACE.

Может быть, цикл разработки: я заметил уродливое диалоговое окно Assert или кто-то мне пожаловался на это, затем я возвращаюсь к коду и выясняю причину возникновения исключения и решаю, как его обработать.

Таким образом, я могу записать МОЙ код за короткое время и защитить меня от неизвестного домена, но всегда быть замеченным, если происходят ненормальные вещи, таким образом, система становится безопасной и более безопасной.

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

Для кода WinForms я всегда соблюдаю золотое правило:

  • Всегда пробуйте / ловите (исключение) код обработчика событий

это защитит ваш пользовательский интерфейс, и вы всегда сможете его использовать.

Для снижения производительности снижение производительности происходит только тогда, когда код достигает catch, выполнение кода попытки без фактического возникшего исключения не имеет значительного эффекта.

Исключение должно быть маловероятным, иначе это не исключение.


-1

Вы должны думать о пользователе. Сбой приложения - это последнийвещь, которую хочет пользователь. Следовательно, любая операция, которая может завершиться ошибкой, должна иметь блок try catch на уровне пользовательского интерфейса. Необязательно использовать try catch в каждом методе, но каждый раз, когда пользователь что-то делает, он должен уметь обрабатывать общие исключения. Это никоим образом не освобождает вас от проверки всего, чтобы предотвратить исключения в первом случае, но нет сложного приложения без ошибок, и ОС может легко добавить неожиданные проблемы, поэтому вы должны предвидеть непредвиденное и убедиться, что пользователь хочет использовать его. операции не будет потери данных из-за сбоя приложения. Нет необходимости когда-либо допускать сбой вашего приложения, если вы перехватите исключения, оно никогда не будет в неопределенном состоянии, и пользователь ВСЕГДА испытывает неудобства из-за сбоя. Даже если исключение находится на самом верхнем уровне, Отсутствие сбоев означает, что пользователь может быстро воспроизвести исключение или, по крайней мере, записать сообщение об ошибке, что значительно поможет вам решить проблему. Конечно, это намного больше, чем просто получить сообщение об ошибке, а затем увидеть только диалоговое окно с ошибкой Windows или что-то в этом роде.

Вот почему вы НИКОГДА не должны зазнаваться и думать, что в вашем приложении нет ошибок, это не гарантируется. И это очень небольшое усилие, чтобы обернуть некоторые блоки try catch вокруг соответствующего кода и показать сообщение об ошибке / зарегистрировать ошибку.

Как пользователь, я, конечно, серьезно злюсь, когда броски, офисное приложение или что-то еще дает сбой. Если исключение настолько велико, что приложение не может продолжить работу, лучше отобразить это сообщение и сообщить пользователю, что делать (перезапустить, исправить некоторые настройки ОС, сообщить об ошибке и т. Д.), Чем просто аварийно завершить работу и все.


Можете ли вы попытаться писать ответы менее напыщенно, а вместо этого попытаться быть более конкретными и попытаться доказать свою точку зрения? Также посмотрите, как ответить .
LuckyLikey
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.