Well, that title is certainly a brain-full and won’t win me any search engine rankings for popular query terms.

Anyway, Nidonocu had an important question he posted on my previous post about the "Do" class:

This looks like this could be quite a useful class but I was wondering how this might work when it comes to wanting to catch specific exceptions rather than just the base Exception type?

I know from using FxCop that using ‘catch (Exception)’ is something Paint.net rather a lot and while I’m sure you sleep at night doing that, its something I’m trying to avoid in my own app. 😉 Are there any simple ways this code could be tweaked to not use Exception?

The issue is that the various TryCatch() helper methods only take a single delegate for the catch clause, and they always take the base Exception type. In essence, the burden of exception type filtering has been placed on that single delegate, forcing you to do something like:

Do.TryCatch(
    () => DoSomething(),
    (ex) =>
    {
        if (ex is InvalidOperationException) { … }
        else if (ex is SystemException) { … }
        else if (ex is FileNotFoundException) { … }
        etc.

    });

Even then, you can’t use "throw" to propagate the exception correctly. This may also encourage lazy exception handling that only covers the generic "oh no something happened" situation. We’re trying to simplify the code, darnit! You see, I knew about this problem, but I didn’t have a solution for it yet. The type of syntax that I wanted was this:

Do.TryCatch(
    () => DoSomething(),
    (InvalidOperationException ex) => { … }
    (SystemException ex) => { … }
    (FileNotFoundException ex) => { … });

This would be very nice and intuitive, and fairly succinct. You would expect the function signature for Do.TryCatch() to look like the following at this point:

void TryCatch(
    Procedure tryFn,
    params Procedure<Exception>[] catchFns);

But you run into a huge problem here. The C# compiler will try to convert each of the lambda expressions into Procedure<Exception> but will be unable to do so — it will convert the first one to Procedure<InvalidOperationException>, etc. and then spit out errors because it can’t convert it. (Actually, it will simply see that the lambda cannot be paired up with Procedure<Exception>, so that last sentence is a weird, technically incorrect simplification.) This is unintuitive at first, but makes perfect sense after thinking about it for a few minutes.

Even if two types, T1 and T2, have an inheritance relationship of T2:T1 (that is, T2 derives from T1), this does not create a relationship between Procedure<T1> and Procedure<T2>. For us, this means that even though InvalidOperationException derives from Exception and is thus substitutable for a parameter that is declared as taking an Exception, it is not the case that Procedure<InvalidOperationException> is substitutable for Procedure<Exception>. They are two distinct types in the .NET runtime. We are doing exception filtering, which is a different problem than just covariance or contravariance (I can never keep those two terms straight, I’m not sure which one applies here).

I was, however, able to set things up to my liking … almost. Here’s the syntax I was finally able to achieve:

Do.TryCatch(
    () => DoSomething(),
    CatchClause.From((InvalidOperationException ex) => CatchResult.Throw),
    CatchClause.From((SystemException ex) => CatchResult.Handled),
    CatchClause.From((FileNotFoundException ex) => { MessageBox.Show("Couldn’t find file!"); return CatchResult.Handled; }));

We are no longer passing a list of delegates to TryCatch(). Instead, we have a CatchClause class (aka functor) that implements a CanHandle() function to allow for filtering, and then a Handle() method that returns CatchResult.Handled to finish the catch clause, or CatchResult.Throw to re-throw the exception.

So, in order to effectively force a type relationship between the delegates (which cannot have "inheritance relationships"), we have to wrap them inside instances of class objects which do have that type relationship we want. In our case, we provide this relationship with a base class called CatchClause, which then has a derived CatchClause<TEx> class, where TEx has a constraint forcing it to derive from Exception. Then, we use CatchClause<InvalidOperationException>, CatchClause<SystemException>, etc. All of these are substitutable for the base CatchClause class, which lets our magic work. Finally, a static helper method in the base CatchClause class makes our TryCatch() usage easier to read and write. Unfortunately we cannot use an implicit type-conversion operator because those cannot take generic type parameters, and the type inference system isn’t quite psychic enough to enable it to work anyway.

Here’s the code, which I hereby release into the public domain:

public enum CatchResult
{
    Handled,
    Throw
}

public abstract class CatchClause
{
    public abstract bool CanHandle(Exception ex);
    public abstract CatchResult Handle(Exception ex);

    public static CatchClause From<TEx>(Function<CatchResult, TEx> catchFunction)
        where TEx : Exception
    {
        return new DelegateToCatchClause<TEx>(catchFunction);
    }
}

public abstract class CatchClause<TEx>
    : CatchClause
      where TEx : Exception
{
    public override bool CanHandle(Exception ex)
    {
        return typeof(TEx).IsAssignableFrom(ex.GetType());
    }

    public abstract CatchResult HandleT(TEx ex);

    public override sealed CatchResult Handle(Exception ex)
    {
        return HandleT((TEx)ex);
    }
}

public class DelegateToCatchClause<TEx>
    : CatchCla
use<TEx>
      where TEx: Exception
{
    private Function<CatchResult, TEx> catchFunction;

    public override CatchResult HandleT(TEx ex)
    {
        return this.catchFunction(ex);
    }

    public DelegateToCatchClause(Function<CatchResult, TEx> catchFunction)
    {
        this.catchFunction = catchFunction;
    }
}

public static void TryCatch(
    Procedure actionProcedure,
    params CatchClause[] catchClauses)
{
    TryCatch(actionProcedure, (IEnumerable<CatchClause>)catchClauses);
}

public static void TryCatch(
    Procedure actionProcedure,
    IEnumerable<CatchClause> catchClauses)
{
    try
    {
        actionProcedure();
    }

    catch (Exception ex)
    {
        foreach (CatchClause catchClause in catchClauses)
        {
            if (catchClause.CanHandle(ex))
            {
                CatchResult catchResult = catchClause.Handle(ex);

                if (catchResult == CatchResult.Handled)
                    break;
                else if (catchResult == CatchResult.Throw)
                    throw;
                else
                    throw new InvalidEnumArgumentException("catchResult", (int)catchResult, typeof(CatchResult));
            }
       }

       throw; // no one handled it, so rethrow
    }
}

Advertisements