开发者

Why don't .NET exceptions work against an interface rather than a base class?

开发者 https://www.devze.com 2023-04-05 15:08 出处:网络
The .Net framework try-catch implementation only allows you to catch types which inherit off the base class \"System.Exception\". Why could this not have been an interface such as \"System.IException\

The .Net framework try-catch implementation only allows you to catch types which inherit off the base class "System.Exception". Why could this not have been an interface such as "System.IException"?

Use case

We use a custom base class in each API that inherits off System.Exception. This is only thrown o开发者_开发知识库nce an exception has been logged and therefore we can easily avoid re-logging by something like:

try
{
    // Do something.
}
catch (LoggedException)
{
    // Already logged so just rethrow.
    throw;
}
catch (Exception ex)
{
    // TODO: Log exception.
    throw new LoggedException("Failed doing something.", ex);
}

This is great until you want a custom exception that inherits off another system exception type such as System.FormatException

The only way to now handle both of these is to have two custom base types and duplicate every catch statement.

Refactoring

If the .net framework just simply looked for something such as System.IException then you could simply have a custom exception interface such as CompanyName.ILoggedException, inheriting System.IException which all your custom exception types implement. Therefore your new catch code would look something like:

try
{
    // Do something.
}
catch (ILoggedException)
{
    // Already logged so just rethrow.
    throw;
}
catch (IException ex)
{
    // TODO: Log exception.
    throw new CustomException("Failed doing something.", ex);
}

Is there a practical reason the framework is implemented like this? Or would this be something to request in a future version of the .Net framework?


As you probably know that in base class case we only have single inheritance but in case of interface a class can implement many interfaces so if you have something like:

class MyException : IExceptionB, IExceptionA
{
}

try
{
 throw new MyException();
}
catch(IExceptionB b){}
catch(IExceptionA b){}

Now this creates ambiguity regarding how to decide which catch handler to call as the exception implements both interface i.e in terms of hierarchy both are at same level unlike the base class where there won't be two class at same level.

The code is hypothetical showing the problem in case Interface based catch was allowed


C#6 introduced exception filtering so what you ask for is now possible in C# (it has long been possible in VB.Net). We can now use the when keyword.

Here is your code refactored to use the new syntax:

try
{
      ...
}
catch (Exception ex) when (!(ex is ILoggedException))
{
    // TODO: Log exception.
    throw new Exception("Failed doing something.", ex);
}

Note that we no longer need the first catch block as it was essentially just a filter and all it did was throw.

Class and interface definitions:

public interface ILoggedException { }

public class CustomLoggedException : Exception, ILoggedException { ... }


One possibly neater workaround that hasn't been mentioned yet is to use extension methods. By harnessing the Exception.Data field, you could neatly discover from a single catch block whether the current exception has been logged yet, and take action as required. This would allow you to construct numerous different company specific exceptions (which are implicitly already logged).

Extension methods required:

private const string ExceptionLoggedKey = "IsExceptionLogged";

public static bool IsLogged(this Exception exception)
{
    if (exception.Data.Contains(ExceptionLoggedKey))
    {
        return (bool)exception.Data[ExceptionLoggedKey];
    }
    return false;
}

public static void SetLogged(this Exception exception)
{
    exception.Data.Add(ExceptionLoggedKey, true);
}

Company exceptions take the following format, setting the IsLogged flag in the constructor:

public class CompanysLoggedException : InvalidOperationException  //Could be any Exception
{
    public CompanysLoggedException()
    {
        this.SetLogged();
    }
}

try/catch usage:

try
{
    throw new ArgumentException("There aren't any arguments.");
}
catch (Exception ex)
{
    if (ex.IsLogged())
        //Nothing additional to do - simply throw the exception
        throw;
    else
        //TODO Log the exception
        throw new CompanysLoggedException();
}

I'd agree that this definitely isn't as neat as the ability to match an exception based on an implemented interface, but I think the pattern is quite concise and readable. Having to remember to add the call to SetLogged() to each new company exception defined is a bit of a flaw though.


I don't really know the reason, but I assume that it's about performance. In case of an exception, each catch block has to check if it machtes the exception. If you allow only types, that's quite simple in case of .Net because you have only single inheritance. Inheritance trees of implemented interfaces can become much more complex.


It is possible in VB to catch interfaces by using something like:

  ' Doesn't require external function, but will will require typecast
  ' if it needs to actually use Ex as IMyExceptionInterface
  Catch Ex As Exception When TypeOf(Ex) Is IMyExceptionInterface
  ' Alternate form: requires pre-declared variable and a function:
  ' Function TryCastIntoSecondParam(Of TSource As Class, TDest As Class) _
  '                                (ByVal Thing As TSource,  ByRef Dest As TDest)
  '   Dim Result As TDest
  '   Result = TryCast(Thing, TDest)
  '   Dest = Result
  '   Return Dest IsNot Nothing
  ' End Function
  Catch Ex As Exception When TryCastIntoSecondParam(Ex, myIMyException)

If the VB or C# compiler implementers wanted to do so, they could allow one to use a syntax

  Catch Ex As IMyExceptionInterface  ' vb
  catch IExceptionInterface ex       ' C#

and implement it with the above code. Even without compiler support, vb users can get the correct semantics with the above code. In C#, it would be necessary to catch an exception, test whether it's the desired type, and rethrow if it isn't; the semantics of that are different from those of using a filter to avoid catching the exception in the first place. Note that for the C# compiler to implement the above constructs, it would have to use filter blocks, but it would not have to expose all the power of filter blocks to programmers--something which the C# implementors have deliberately refused to do.

All that having been said, I suspect the answer to the original question might be "the designers couldn't think of any good use cases for such a thing", or it might be "Interfaces require more complex type resolution than do classes, creating the possibility that the mere act of deciding whether to catch an exception could fail with an exception of its own."

Actually, I happen to dislike the use of class types as a means of deciding what exceptions to catch, since the question of whether or not to catch an exception is often largely orthogonal to the question of what precisely happened to cause it. If an attempt to load a document fails, I'm not nearly as interested in the question of whether some argument was out of range or some index was out of range, as I am in the question of whether I can safely recover from the attempt by pretending I'd never made it. What's really needed is a "severity" measure for exceptions which can be increased or decreased as the exception bubbles up the call chain. Such a thing might be somewhat practical in vb.net (which has exception filters) but probably not in C# (which doesn't), but would be limited in any case by the lack of any support within the built-in exceptions.

Edit/Addendum

It is possible to use exception filters within a C# project if one uses a DLL written in vb to implement a try/filter/catch/finally wrapper which calls some delegates. Unfortunately, using such a DLL would require some tradeoffs between run-time efficiency and code legibility. I hadn't thought about implementing such a DLL for the particular optimized purpose of catching an arbitrary number of interfaces; I don't know whether there would be any advantage to including such functionality in the DLL, versus passing it a lambda expression or anonymous method to test whether an exception should be caught.

BTW, another feature a wrapper can provide which is otherwise missing in C# is the ability to report a double-fault condition (an exception occurs in the mainline, and another exception occurs during a subsequent "finally" block) without having to catch the initial exception. When exception occurs in a finally block, that's generally a bigger problem than an exception which occurs in the main-line, and should thus not be stifled, but allowing a "finally block" exception to percolate up the call stack would generally destroy any evidence of the original exception. While surrounding code will likely be more interested in the cleanup failure than the original exception, logging both exceptions is apt to be far more useful than stifling the original.


An Exception is much more than just a way of letting your code know that something has gone wrong. It contains all kinds of debug info (stack trace, target, inner exception, hresult, Watson buckets, etc) and this has to be collected SOMEWHERE. The most logical and simple solution is to let the base class collect it.


This is almost you mentioned, without some "code sugar" thought:

try
{
}
catch(LoggerException ex) 
{ 
    ex.WriteLog(); 
} 
catch(Exception ex)
{
    ILoggerException1 l1 = ex as ILoggerException1; 
    if (l1 != null)
    {
        l1.WriteLog1();
    }
    else
    {
        ILoggerException2 l2 = ex as ILoggerException2; 
        if (l2 != null)
        {
            l2.WriteLog2();
        }
        else
        {
            ILoggerException3 l3 = ex as ILoggerException3; 
            if (l3 != null)
            {
                l3.WriteLog3();
            }
            else
            {
                throw ex;
            }
        }
    }
}

With support form the compiler it whould be written as:

try
{
}
catch(LoggerException ex)
{
    ex.WriteLog();
}
// no more classes is allowed by the compiler be here, only interfaces in a tail catch recursion
catch(ILoggerException1 ex1)
{
    ex1.WriteLog();
}
catch(ILoggerException2 ex2)
{
    ex2.WriteLog();
}
catch(ILoggerException3 ex3)
{
    ex3.WriteLog();
}


Turns out there is an interface ever since framework 2.0:

System.Runtime.InteropServices._Exception

For more information on this interface go here.

I used a workaround though and used an abstract class and implemented the class. Feels dirty to me but it was actually an easier implementation for what I needed.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号