使用回调而不是抛出异常?

时间:2013-06-27 15:23:08

标签: c# .net design-patterns

我正在考虑使用回调而不是在C#/ .NET中抛出异常。

优点和缺点

优点是

  • 没有像未经检查的异常的控制流那样隐藏的转到
  • 更清晰的代码,特别是如果涉及多个例外
  • 抛出的异常记录在方法签名中,并且调用者被迫考虑处理异常,但可以轻松地传递应用程序范围的异常处理程序,“UnhandledExceptionHandler”或null。因此它们有点像“软”检查异常,但更易于维护,因为异常可以通过重载方法抛出,或者异常可以通过不再在异常处理程序上调用“handle”来消除。
  • 也适用于异步调用
  • 异常处理程序可以处理在不同位置抛出的几个异常
  • 明确指出应该处理哪些异常。普通方式抛出异常仍可用于您不希望像“NotImplementedException”那样处理的异常。

缺点是

  • 不是C#和.NET的惯用语
  • throw方法必须通过立即返回返回值来中断控制流。如果返回类型是值类型,则很难。
  • ? (见下面的问题)

问题

我可能缺少一些关键的缺点,因为我想知道为什么不使用它。我错过了什么缺点?

实施例

而不是

void ThrowingMethod() {
    throw new Exception();
}

void CatchingMethod() {
    try {
         ThrowingMethod();
    } catch(Exception e) {
         //handle exception
    }
}

我愿意

void ThrowingMethod(ExceptionHandler exceptionHandler) {
    exceptionHandler.handle(new Exception());
}

void CatchingMethod() {
     ThrowingMethod(exception => */ handle exception */ );
}

delegate void ExceptionHandler(Exception exception);

在某处定义并且“handle(...)”是一个检查null的扩展方法,检索堆栈跟踪,如果在抛出异常时根本没有异常处理程序,则可能抛出“UnhandledException”。 p>


之前未抛出异常的方法中抛出异常的示例
void UsedToNotThrowButNowThrowing() {
   UsedToNotThrowButNowThrowing(null);
}

//overloads existing method that did not throw to now throw
void UsedToNotThrowButNowThrowing(ExceptionHandler exceptionHandler) {
    //extension method "handle" throws an UnhandledException if the handler is null
    exceptionHandler.handle(exceptionHandler);
}

返回值

的方法示例
TResult ThrowingMethod(ExceptionHandler<TResult> exceptionHandler) {
        //code before exception
        return exceptionHandler.handle(new Exception()); //return to interrupt execution
        //code after exception
    }

TResult CatchingMethod() {
     return ThrowingMethod(exception => */ handle exception and return value */ );
}

delegate TResult ExceptionHandler<TResult>(Exception exception);

3 个答案:

答案 0 :(得分:0)

首先,您需要将这些处理程序的开销传递给应用程序中的几乎所有方法。这是一个非常重量级的依赖,以及在构建应用程序之前做出的决定。

其次,存在处理系统抛出异常和第三方程序集的其他异常的问题。

第三,异常是指在抛出程序时暂停程序的执行,因为它确实是“异常”,而不仅仅是可以处理的错误,允许继续执行。

答案 1 :(得分:0)

<强>可扩展性。

正如@mungflesh正确地指出你必须传递这些处理程序。我的第一个问题不是开销,而是可伸缩性:它会影响方法签名。它可能会导致与我们在Java中使用已检查异常相同的可伸缩性问题(我不了解C#,我只做C ++和一些Java)。

想象一下一个深度为50次调用的调用堆栈(没有什么极端的,IMO)。有一天,一个变化出现了,并且没有抛出的链中的一个被调用者变成了一个现在可以抛出异常的方法。如果它是未经检查的异常,您只需更改顶级代码即可处理新错误。如果它是已检查的异常或您应用了您的想法,则必须通过调用链更改所有涉及的方法签名。不要忘记签名更改会传播:您更改这些方法的签名,您必须在调用这些方法的其他地方更改代码,可能会生成更多签名更改。简而言之,规模很差。 :(


这是一些伪代码,显示了我的意思。使用未经检查的异常,您可以通过以下方式处理深度为50的callstack中的更改:

f1() {
  try {    // <-- This try-catch block is the only change you have to make
    f2();  
  }
  catch(...) {
    // do something with the error
  }
}

f2() { // None of the f2(), f3(), ..., f49() has to be changed
  f3();
}

...

f49() {
  f50();
}

f50() {
  throw SomeNewException; // it was not here before
}

使用您的方法处理相同的更改:

f1() {
  ExceptionHandler h;
  f2(h);
}

f2(ExceptionHandler h) { // Signature change
  f3(h); // Calling site change
}

...

f49(ExceptionHandler h) { // Signature change
  f50(h); // Calling site change
}

f50(ExceptionHandler h) {
  h.SomeNewException(); // it was not here before
}

所涉及的所有方法(f2 ... f49)现在都有一个新的签名,并且呼叫站点也必须更新(例如f2()变为f2(h)等) 。请注意,f2...f49甚至不应该知道此更改,但是,他们的签名和调用网站都必须更改。


换句话说:所有中间调用现在必须处理错误处理程序,即使它是一个他们甚至不知道的细节。使用未经检查的例外情况,可以隐藏这些详细信息。


未经检查的异常确实&#34;隐藏了如控制流&#34;但至少他们的规模很好。毫无疑问,很快就会导致难以维持的混乱...

+1虽然,这是一个有趣的想法。

答案 2 :(得分:0)

如果我做对了,如果一个方法中有两个可能的异常,则该方法需要采用两个不同的参数。为了模拟检查的异常并使通知者可能的异常,必须为不同类型的可能的异常传递不同的处理程序。因此,在多态情况下,当您定义接口或抽象类时,您将可能的异常强加给未编写的代码,因此不允许具体实现生成新类型的异常。

作为示例,您正在实现Stream类和FileStream具体类。您必须为文件未找到异常传递异常处理程序,这是不好的,因为它迫使MemoryStream接受文件未找到异常处理程序,或者,另一方面,您不允许在其中生成文件未找到异常。 FileStream,因为签名不允许这样做。