5

主意

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

优点和缺点

优点是

  • 没有隐藏的 goto 类似未检查异常的控制流
  • 更简洁的代码,尤其是在涉及多个异常的情况下
  • 抛出的异常记录在方法签名中,调用者被迫考虑处理异常,但可以轻松地将应用程序范围的异常处理程序、“UnhandledExceptionHandler”或 null 传递给它们。所以它们有点像“软”检查异常,但更易于维护,因为可以通过重载方法之后抛出异常,或者可以通过不再在异常处理程序上调用“handle”来删除异常)。
  • 也适用于异步调用
  • 异常处理程序可以处理在不同位置抛出的多个异常
  • 明确应处理哪些异常。以普通方式抛出异常仍可用于您不想处理的异常,例如“NotImplementedException”。

缺点是

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

问题

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

例子:

代替

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”。


在之前没有抛出异常的方法中抛出异常的示例

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);
4

3 回答 3

0

可扩展性。

正如@mungflesh 正确指出的那样,您必须将这些处理程序传递给. 我首先关心的不是开销而是可扩展性:它会影响方法签名。它可能会导致与我们在 Java 中检查异常时遇到的相同的可伸缩性问题(我不了解 C#,我只使用 C++ 和一些 Java)。

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


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

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甚至不应该知道此更改,但是,他们的签名和调用站点都必须更改。


换句话说:所有中间调用现在都必须处理错误处理程序,即使这是他们甚至不应该知道的细节。对于未经检查的异常,可以隐藏这些详细信息。


未经检查的异常确实是“隐藏的 goto 控制流”,但至少它们可以很好地扩展。毫无疑问,会很快导致无法维护的混乱......

+1 不过,一个有趣的想法。

于 2013-06-27T15:56:44.847 回答
0

一方面,您将需要将这些处理程序传递给应用程序中的几乎每个方法的开销。这是一个非常重量级的依赖,也是在构建应用程序之前要做出的生死攸关的决定。

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

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

于 2013-06-27T15:42:16.687 回答
0

如果我做对了,如果一个方法中有两个可能的异常,那么该方法需要接受两个不同的参数。为了模拟已检查的异常并让调用者了解可能的异常,您必须为不同类型的可能异常传递不同的处理程序。因此在多态的情况下,当你定义一个接口或者一个抽象类的时候,你把可能的异常强加给了未编写的代码,所以具体的实现是不允许产生新的异常类型的。

例如,认为您正在实现 Stream 类和 FileStream 具体类。您必须为文件未找到异常传递一个异常处理程序,这很糟糕,因为它强制 MemoryStream 接受文件未找到异常处理程序,或者另一方面,您不允许在中生成文件未找到异常FileStream,因为签名不允许。

于 2020-08-06T19:43:19.413 回答