Catch multiple exceptions at once?

前端 未结 27 2411
夕颜
夕颜 2020-11-22 11:31

It is discouraged to simply catch System.Exception. Instead, only the "known" exceptions should be caught.

Now, this sometimes leads to unnecce

27条回答
  •  暖寄归人
    2020-11-22 12:09

    Since I felt like these answers just touched the surface, I attempted to dig a bit deeper.

    So what we would really want to do is something that doesn't compile, say:

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

    The reason we want this is because we don't want the exception handler to catch things that we need later on in the process. Sure, we can catch an Exception and check with an 'if' what to do, but let's be honest, we don't really want that. (FxCop, debugger issues, uglyness)

    So why won't this code compile - and how can we hack it in such a way that it will?

    If we look at the code, what we really would like to do is forward the call. However, according to the MS Partition II, IL exception handler blocks won't work like this, which in this case makes sense because that would imply that the 'exception' object can have different types.

    Or to write it in code, we ask the compiler to do something like this (well it's not entirely correct, but it's the closest possible thing I guess):

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

    The reason that this won't compile is quite obvious: what type and value would the '$exception' object have (which are here stored in the variables 'e')? The way we want the compiler to handle this is to note that the common base type of both exceptions is 'Exception', use that for a variable to contain both exceptions, and then handle only the two exceptions that are caught. The way this is implemented in IL is as 'filter', which is available in VB.Net.

    To make it work in C#, we need a temporary variable with the correct 'Exception' base type. To control the flow of the code, we can add some branches. Here goes:

        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.");
    

    The obvious disadvantages for this are that we cannot re-throw properly, and -well let's be honest- that it's quite the ugly solution. The uglyness can be fixed a bit by performing branch elimination, which makes the solution slightly better:

    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 :-)");
    }
    

    That leaves just the 're-throw'. For this to work, we need to be able to perform the handling inside the 'catch' block - and the only way to make this work is by an catching 'Exception' object.

    At this point, we can add a separate function that handles the different types of Exceptions using overload resolution, or to handle the Exception. Both have disadvantages. To start, here's the way to do it with a helper function:

    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.");
    

    And the other solution is to catch the Exception object and handle it accordingly. The most literal translation for this, based on the context above is this:

    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;
        }
    }
    

    So to conclude:

    • If we don't want to re-throw, we might consider catching the right exceptions, and storing them in a temporary.
    • If the handler is simple, and we want to re-use code, the best solution is probably to introduce a helper function.
    • If we want to re-throw, we have no choice but to put the code in a 'Exception' catch handler, which will break FxCop and your debugger's uncaught exceptions.

提交回复
热议问题