Поймать несколько исключений одновременно? - PullRequest
1901 голосов
/ 26 сентября 2008

Не рекомендуется просто ловить System.Exception. Вместо этого должны быть обнаружены только «известные» исключения.

Теперь это иногда приводит к ненужному повторяющемуся коду, например:

try
{
    WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
    WebId = Guid.Empty;
}
catch (OverflowException)
{
    WebId = Guid.Empty;
}

Интересно: есть ли способ перехватить оба исключения и вызвать WebId = Guid.Empty только один раз?

Данный пример довольно прост, так как это всего лишь GUID. Но представьте себе код, в котором вы несколько раз модифицируете объект, и если одна из манипуляций не удалась ожидаемым образом, вы хотите «сбросить» object. Однако, если есть непредвиденное исключение, я все еще хочу бросить это выше.

Ответы [ 28 ]

18 голосов
/ 04 октября 2015

в C # 6 рекомендуется использовать фильтры исключений, вот пример:

 try
 {
      throw new OverflowException();
 }
 catch(Exception e ) when ((e is DivideByZeroException) || (e is OverflowException))
 {
       // this will execute iff e is DividedByZeroEx or OverflowEx
       Console.WriteLine("E");
 }
17 голосов
/ 08 декабря 2014

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

catch(Exception ex)
{   
    if (!(ex is SomeException || ex is OtherException)) throw;

    // Handle exception
}

Есть несколько преимуществ обращения выражения:

  • Инструкция возврата не обязательна
  • Код не является вложенным
  • Нет риска забыть утверждения «throw» или «return», которые в решении Иосифа отделены от выражения.

Он даже может быть сжат до одной строки (хотя и не очень красиво)

catch(Exception ex) { if (!(ex is SomeException || ex is OtherException)) throw;

    // Handle exception
}

Edit: фильтрация исключений в C # 6.0 сделает синтаксис немного чище и будет иметь ряд других преимуществ по сравнению с любым текущим решением. (прежде всего оставляя стек без повреждений)

Вот как будет выглядеть та же проблема с использованием синтаксиса C # 6.0:

catch(Exception ex) when (ex is SomeException || ex is OtherException)
{
    // Handle exception
}
17 голосов
/ 01 сентября 2012

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

public void TryCatch(...)
{
    try
    {
       // something
       return;
    }
    catch (FormatException) {}
    catch (OverflowException) {}

    WebId = Guid.Empty;
}

Будут сгенерированы любые другие исключения, и код WebId = Guid.Empty; не будет введен. Если вы не хотите, чтобы другие исключения вызывали сбой вашей программы, просто добавьте это ПОСЛЕ двух других зацепок:

...
catch (Exception)
{
     // something, if anything
     return; // only need this if you follow the example I gave and put it all in a method
}
16 голосов
/ 26 сентября 2008

@ Мичил

Немного переработанная версия вашего кода:

catch (Exception ex)
{
   Type exType = ex.GetType();
   if (exType == typeof(System.FormatException) || 
       exType == typeof(System.OverflowException)
   {
       WebId = Guid.Empty;
   } else {
      throw;
   }
}

Сравнение строк уродливо и медленно.

13 голосов
/ 26 сентября 2008

Как насчет

try
{
    WebId = Guid.Empty;
    WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
11 голосов
/ 07 января 2009
catch (Exception ex)
{
    if (!(
        ex is FormatException ||
        ex is OverflowException))
    {
        throw;
    }
    Console.WriteLine("Hello");
}
11 голосов
/ 18 мая 2013

Предупреждение и предупреждение: Еще один функциональный стиль.

То, что в ссылке, не дает прямого ответа на ваш вопрос, но тривиально расширить его, чтобы оно выглядело так:

static void Main() 
{ 
    Action body = () => { ...your code... };

    body.Catch<InvalidOperationException>() 
        .Catch<BadCodeException>() 
        .Catch<AnotherException>(ex => { ...handler... })(); 
}

(по сути, предоставляет еще одну пустую перегрузку Catch, которая возвращает себя)

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

9 голосов
/ 28 ноября 2013

Обновление 2015-12-15: См. https://stackoverflow.com/a/22864936/1718702 для C # 6. Это более чистый и теперь стандартный язык.

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

У меня уже было это расширение в моей библиотеке, изначально написанное для других целей, но оно прекрасно работало для type проверки исключений. Плюс, имхо, это выглядит чище, чем куча || заявлений. Кроме того, в отличие от принятого ответа, я предпочитаю явную обработку исключений, чтобы ex is ... имел нежелательное поведение, поскольку производные классы присваиваются родительским типам).

Использование

if (ex.GetType().IsAnyOf(
    typeof(FormatException),
    typeof(ArgumentException)))
{
    // Handle
}
else
    throw;

Расширение IsAnyOf.cs (см. Пример полной обработки ошибок для зависимостей)

namespace Common.FluentValidation
{
    public static partial class Validate
    {
        /// <summary>
        /// Validates the passed in parameter matches at least one of the passed in comparisons.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_comparisons">Values to compare against.</param>
        /// <returns>True if a match is found.</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
        {
            // Validate
            p_parameter
                .CannotBeNull("p_parameter");
            p_comparisons
                .CannotBeNullOrEmpty("p_comparisons");

            // Test for any match
            foreach (var item in p_comparisons)
                if (p_parameter.Equals(item))
                    return true;

            // Return no matches found
            return false;
        }
    }
}

Пример полной обработки ошибок (копирование-вставка в новое консольное приложение)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common.FluentValidation;

namespace IsAnyOfExceptionHandlerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // High Level Error Handler (Log and Crash App)
            try
            {
                Foo();
            }
            catch (OutOfMemoryException ex)
            {
                Console.WriteLine("FATAL ERROR! System Crashing. " + ex.Message);
                Console.ReadKey();
            }
        }

        static void Foo()
        {
            // Init
            List<Action<string>> TestActions = new List<Action<string>>()
            {
                (key) => { throw new FormatException(); },
                (key) => { throw new ArgumentException(); },
                (key) => { throw new KeyNotFoundException();},
                (key) => { throw new OutOfMemoryException(); },
            };

            // Run
            foreach (var FooAction in TestActions)
            {
                // Mid-Level Error Handler (Appends Data for Log)
                try
                {
                    // Init
                    var SomeKeyPassedToFoo = "FooParam";

                    // Low-Level Handler (Handle/Log and Keep going)
                    try
                    {
                        FooAction(SomeKeyPassedToFoo);
                    }
                    catch (Exception ex)
                    {
                        if (ex.GetType().IsAnyOf(
                            typeof(FormatException),
                            typeof(ArgumentException)))
                        {
                            // Handle
                            Console.WriteLine("ex was {0}", ex.GetType().Name);
                            Console.ReadKey();
                        }
                        else
                        {
                            // Add some Debug info
                            ex.Data.Add("SomeKeyPassedToFoo", SomeKeyPassedToFoo.ToString());
                            throw;
                        }
                    }
                }
                catch (KeyNotFoundException ex)
                {
                    // Handle differently
                    Console.WriteLine(ex.Message);

                    int Count = 0;
                    if (!Validate.IsAnyNull(ex, ex.Data, ex.Data.Keys))
                        foreach (var Key in ex.Data.Keys)
                            Console.WriteLine(
                                "[{0}][\"{1}\" = {2}]",
                                Count, Key, ex.Data[Key]);

                    Console.ReadKey();
                }
            }
        }
    }
}

namespace Common.FluentValidation
{
    public static partial class Validate
    {
        /// <summary>
        /// Validates the passed in parameter matches at least one of the passed in comparisons.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_comparisons">Values to compare against.</param>
        /// <returns>True if a match is found.</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
        {
            // Validate
            p_parameter
                .CannotBeNull("p_parameter");
            p_comparisons
                .CannotBeNullOrEmpty("p_comparisons");

            // Test for any match
            foreach (var item in p_comparisons)
                if (p_parameter.Equals(item))
                    return true;

            // Return no matches found
            return false;
        }

        /// <summary>
        /// Validates if any passed in parameter is equal to null.
        /// </summary>
        /// <param name="p_parameters">Parameters to test for Null.</param>
        /// <returns>True if one or more parameters are null.</returns>
        public static bool IsAnyNull(params object[] p_parameters)
        {
            p_parameters
                .CannotBeNullOrEmpty("p_parameters");

            foreach (var item in p_parameters)
                if (item == null)
                    return true;

            return false;
        }
    }
}

namespace Common.FluentValidation
{
    public static partial class Validate
    {
        /// <summary>
        /// Validates the passed in parameter is not null, throwing a detailed exception message if the test fails.
        /// </summary>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public static void CannotBeNull(this object p_parameter, string p_name)
        {
            if (p_parameter == null)
                throw
                    new
                        ArgumentNullException(
                        string.Format("Parameter \"{0}\" cannot be null.",
                        p_name), default(Exception));
        }
    }
}

namespace Common.FluentValidation
{
    public static partial class Validate
    {
        /// <summary>
        /// Validates the passed in parameter is not null or an empty collection, throwing a detailed exception message if the test fails.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public static void CannotBeNullOrEmpty<T>(this ICollection<T> p_parameter, string p_name)
        {
            if (p_parameter == null)
                throw new ArgumentNullException("Collection cannot be null.\r\nParameter_Name: " + p_name, default(Exception));

            if (p_parameter.Count <= 0)
                throw new ArgumentOutOfRangeException("Collection cannot be empty.\r\nParameter_Name: " + p_name, default(Exception));
        }

        /// <summary>
        /// Validates the passed in parameter is not null or empty, throwing a detailed exception message if the test fails.
        /// </summary>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
        /// <exception cref="ArgumentException"></exception>
        public static void CannotBeNullOrEmpty(this string p_parameter, string p_name)
        {
            if (string.IsNullOrEmpty(p_parameter))
                throw new ArgumentException("String cannot be null or empty.\r\nParameter_Name: " + p_name, default(Exception));
        }
    }
}

Два образца модульных тестов NUnit

Соответствующее поведение для Exception типов является точным (т. Е. Дочерний элемент НЕ соответствует ни одному из своих родительских типов).

using System;
using System.Collections.Generic;
using Common.FluentValidation;
using NUnit.Framework;

namespace UnitTests.Common.Fluent_Validations
{
    [TestFixture]
    public class IsAnyOf_Tests
    {
        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void IsAnyOf_ArgumentNullException_ShouldNotMatch_ArgumentException_Test()
        {
            Action TestMethod = () => { throw new ArgumentNullException(); };

            try
            {
                TestMethod();
            }
            catch (Exception ex)
            {
                if (ex.GetType().IsAnyOf(
                    typeof(ArgumentException), /*Note: ArgumentNullException derrived from ArgumentException*/
                    typeof(FormatException),
                    typeof(KeyNotFoundException)))
                {
                    // Handle expected Exceptions
                    return;
                }

                //else throw original
                throw;
            }
        }

        [Test, ExpectedException(typeof(OutOfMemoryException))]
        public void IsAnyOf_OutOfMemoryException_ShouldMatch_OutOfMemoryException_Test()
        {
            Action TestMethod = () => { throw new OutOfMemoryException(); };

            try
            {
                TestMethod();
            }
            catch (Exception ex)
            {
                if (ex.GetType().IsAnyOf(
                    typeof(OutOfMemoryException),
                    typeof(StackOverflowException)))
                    throw;

                /*else... Handle other exception types, typically by logging to file*/
            }
        }
    }
}
7 голосов
/ 21 октября 2014

Поскольку я чувствовал, что эти ответы только коснулись поверхности, я попытался копнуть немного глубже.

Итак, что мы действительно хотим сделать, это то, что не компилируется, скажем:

// Won't compile... damn
public static void Main()
{
    try
    {
        throw new ArgumentOutOfRangeException();
    }
    catch (ArgumentOutOfRangeException)
    catch (IndexOutOfRangeException) 
    {
        // ... handle
    }

Причина, по которой мы хотим этого, заключается в том, что мы не хотим, чтобы обработчик исключений перехватывал вещи, которые нам понадобятся позже в процессе. Конечно, мы можем поймать исключение и проверить, что делать, если «да», но давайте будем честными, мы этого не хотим. (FxCop, проблемы с отладчиком, уродство)

Так почему этот код не скомпилируется - и как мы можем взломать его таким образом, что он будет?

Если мы посмотрим на код, то, что мы действительно хотели бы сделать, это переадресовать вызов. Однако, согласно MS Partition II, блоки обработчиков исключений IL не будут работать таким образом, что в этом случае имеет смысл, поскольку это подразумевает, что объект «исключения» может иметь разные типы.

Или, чтобы записать это в коде, мы просим компилятор сделать что-то вроде этого (ну, это не совсем правильно, но я думаю, это самая близкая вещь):

// Won't compile... damn
try
{
    throw new ArgumentOutOfRangeException();
}
catch (ArgumentOutOfRangeException e) {
    goto theOtherHandler;
}
catch (IndexOutOfRangeException e) {
theOtherHandler:
    Console.WriteLine("Handle!");
}

Причина, по которой это не скомпилируется, совершенно очевидна: какой тип и значение будет иметь объект '$ exception' (который здесь хранится в переменных 'e')? Мы хотим, чтобы компилятор справился с этим, отметив, что общим базовым типом обоих исключений является «Исключение», используйте его для переменной, содержащей оба исключения, а затем обрабатывайте только два захваченных исключения. Способ, которым это реализовано в IL, является «фильтром», который доступен в VB.Net.

Чтобы он работал в C #, нам нужна временная переменная с правильным базовым типом «Exception». Чтобы контролировать поток кода, мы можем добавить несколько веток. Здесь идет:

    Exception ex;
    try
    {
        throw new ArgumentException(); // for demo purposes; won't be caught.
        goto noCatch;
    }
    catch (ArgumentOutOfRangeException e) {
        ex = e;
    }
    catch (IndexOutOfRangeException e) {
        ex = e;
    }

    Console.WriteLine("Handle the exception 'ex' here :-)");
    // throw ex ?

noCatch:
    Console.WriteLine("We're done with the exception handling.");

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

Exception ex = null;
try
{
    throw new ArgumentException();
}
catch (ArgumentOutOfRangeException e)
{
    ex = e;
}
catch (IndexOutOfRangeException e)
{
    ex = e;
}
if (ex != null)
{
    Console.WriteLine("Handle the exception here :-)");
}

Это оставляет только «переброс». Чтобы это работало, нам нужно иметь возможность выполнять обработку внутри блока «catch» - и единственный способ выполнить эту работу - перехватывать объект «Exception».

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

private static bool Handle(Exception e)
{
    Console.WriteLine("Handle the exception here :-)");
    return true; // false will re-throw;
}

public static void Main()
{
    try
    {
        throw new OutOfMemoryException();
    }
    catch (ArgumentException e)
    {
        if (!Handle(e)) { throw; }
    }
    catch (IndexOutOfRangeException e)
    {
        if (!Handle(e)) { throw; }
    }

    Console.WriteLine("We're done with the exception handling.");

И другое решение - перехватить объект Exception и обработать его соответствующим образом. Наиболее буквальный перевод для этого, основанный на контексте выше, это:

try
{
    throw new ArgumentException();
}
catch (Exception e)
{
    Exception ex = (Exception)(e as ArgumentException) ?? (e as IndexOutOfRangeException);
    if (ex != null)
    {
        Console.WriteLine("Handle the exception here :-)");
        // throw ?
    }
    else 
    {
        throw;
    }
}

Итак, подытожим:

  • Если мы не хотим перебрасывать, мы могли бы рассмотреть возможность перехвата правильных исключений и их временного хранения.
  • Если обработчик прост и мы хотим повторно использовать код, лучшим решением, вероятно, является введение вспомогательной функции.
  • Если мы хотим перебросить, у нас нет другого выбора, кроме как поместить код в обработчик перехвата 'Exception', который сломает FxCop и необработанные исключения вашего отладчика.
6 голосов
/ 15 ноября 2017

Это классическая проблема, с которой сталкивается каждый разработчик C #.

Позвольте мне разбить ваш вопрос на 2 вопроса. Первый,

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

Короче, нет.

Что приводит к следующему вопросу,

Как избежать написания дублирующего кода, если я не могу перехватить несколько типов исключений в одном блоке catch ()?

Учитывая вашу конкретную выборку, где резервное значение дешево построить, я хотел бы выполнить следующие шаги:

  1. Инициализировать WebId в качестве запасного значения.
  2. Создайте новый Guid во временной переменной.
  3. Установите для WebId полностью созданную временную переменную. Сделайте это последним утверждением блока try {}.

Код выглядит так:

try
{
    WebId = Guid.Empty;
    Guid newGuid = new Guid(queryString["web"]);
    // More initialization code goes here like 
    // newGuid.x = y;
    WebId = newGuid;
}
catch (FormatException) {}
catch (OverflowException) {}

Если выдается какое-либо исключение, то для WebId никогда не устанавливается половинное значение и остается Guid.Empty.

Если создание запасного значения стоит дорого, а сброс значения намного дешевле, я бы переместил код сброса в его собственную функцию:

try
{
    WebId = new Guid(queryString["web"]);
    // More initialization code goes here.
}
catch (FormatException) {
    Reset(WebId);
}
catch (OverflowException) {
    Reset(WebId);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...