你更喜欢像Java中的检查异常处理还是像C#中的未检查异常处理?为什么?
你更喜欢像Java中的检查异常处理还是像C#中的未检查异常处理?为什么?
没什么用。
当正确使用时,被检查异常是很好的东西,但更多时候它们会导致以下问题:
doSomething();
try
{
somethingThrowsCheckedException();
}
catch(ThatCheckedException)
{ }
doSomethingElse();
坦率地说,那样做是不对的。你应该让你无法处理的异常上浮。
如果正确使用被检查异常,它们可以很好地发挥作用。但是非常频繁地,正确处理被检查异常的结果是出现像这样的方法签名:
public void itMightThrow() throws Exception1, Exception2, Exception3, Exception4, // ...
Exception12, Exception13, /* ... */ Exception4499379874
{
// body
}
我夸张了吗?只是稍微有点。
编辑:
话虽如此,当涉及到异常处理时,我更喜欢C#而不是Java的一件事与受检异常无关(如果我选择Spec#,我可以得到它)。不,我喜欢的是,在C#中,堆栈跟踪是在您抛出异常时填充的,而不是像Java一样在实例化异常时填充的。
编辑2: 这是针对评论者@ Yishai,@ Eddie,@ Bill K的:
首先,您应该查看此线程以获取有关如何在不实例化异常的情况下获取堆栈跟踪的信息。请记住,遍历堆栈是一项繁重的过程,不应定期执行。
第二,我喜欢C#的异常堆栈跟踪在抛出异常时填充而不是在实例化时填充的原因是您可以做以下事情:
private MyException NewException(string message)
{
MyException e = new MyException(message);
Logger.LogException(message, e);
return e;
}
// and elsewhere...
if(mustThrow)
{
throw NewException("WHOOOOPSIEE!");
}
如果没有在堆栈跟踪中包含NewException
方法,那么在Java中你无法执行这个技巧。
对于那些无法预测的事情出错时,我更喜欢使用已检查异常。例如,IOException或SQLException。它告诉程序员他们必须考虑到可能会发生不可预测的错误,无论他们如何努力编写健壮的代码都无法避免抛出异常。
然而,太多时候程序员认为已检查异常是一种语言机制要去处理,但实际上(在良好设计的API中)它是表明操作中存在不可预测行为的指示,你应该依赖于确定性结果,也就是在相同的输入条件下始终能够正常运行该操作。
话虽如此,在实践中,已检查异常遇到了两个问题:
throw new RuntimeException(e);
当他们不知道该怎么做时。
此外,已检查异常是另一个API设计元素,可能出现设计缺陷,而API的用户必须忍受这种缺陷。
编辑:另一个答案指出了两个激发C#设计决策(不使用已检查异常)的问题。在我看来,这两个论点都非常糟糕,所以我认为有必要进行处理/平衡。
我认为这两个版本都存在未解决的问题,因为当那些评论被发表时,人们已经接受了处理已检查异常上移至下一级的正确方法是通过包装适用于API抽象的不同已检查异常。例如,如果您有一个存储API可以处理IOException、SQLException或XML相关异常,那么一个正确设计的API将在通用的PersistanceException或类似内容后面隐藏这些差异。
除了这些一般的设计指导方针之外,在具体情况下,这些论点确实引起了许多关于替代方案的问题:
这是Web应用程序的一个很好的设计,因为死亡并显示给用户一个漂亮的错误500页面就足够了(因为交易已由容器处理)。但并非所有应用程序都考虑到这些要求。
论点最终归结为(至少对我来说):不要担心异常,任何事情都可能出错,只需构建一个catch-all。
那好吧。这就是已检查和未检查异常方法之间的核心区别。已检查异常向程序员发出提示,指出易变且不可预测的调用。未检查异常方法则假定所有错误条件都属于同一类别,它们只是具有不同的名称,并使它们未经检查,以便没有人会绕过它们。
现在这些争论在CLR层面上确实有道理。我同意所有检查异常都应该在编译器层面上,而不是运行时层面上。
我从未使用过Java,但自从我阅读了以下内容:
我非常确定我不喜欢受检异常(在当前实现中)。
提到的两个主要观点如下。
版本兼容性
Anders Hejlsberg:让我们从版本控制开始,因为那里的问题很容易看到。假设我创建了一个名为foo的方法,声明它会抛出异常A、B和C。在foo的第二个版本中,我想添加一堆功能,现在foo可能会抛出异常D。为了将D添加到该方法的throws子句中,我必须进行破坏性更改,因为调用该方法的现有客户端几乎肯定不会处理该异常。
在新版本中向throws子句添加新异常会破坏客户端代码。这就像向接口添加方法一样。发布接口后,它在所有实际用途上都是不可变的,因为任何实现可能具有您想要在下一个版本中添加的方法。因此,您必须创建一个全新的接口。同样,在异常情况下,您要么必须创建一个名为foo2的全新方法,该方法会抛出更多异常,要么必须在新foo中捕获异常D,并将其转换为A、B或C。
可扩展性
Anders Hejlsberg:可扩展性问题与版本兼容性问题有些相关。从小的方面来看,受检异常非常诱人。通过一个简单的示例,您可以显示您已经检查了FileNotFoundException,这不是很好吗?当您只调用一个API时,这很好。但是,当您开始构建涉及四到五个不同子系统的大型系统时,问题就开始了。每个子系统都会抛出四到十个异常。现在,每次您走上聚合阶梯时,都会有一个指数级别的异常层次结构在您下面。您最终不得不声明40个可能抛出的异常。一旦将其与另一个子系统聚合,您就会在throws子句中拥有80个异常。它就会失控。
在大型系统中,受检异常变得非常烦人,以至于人们完全绕过了该功能。他们要么在所有地方都说“throws Exception”,要么——我无法告诉您我见过多少次——他们说:“尝试,da da da da da,catch curly curly。”他们认为,“哦,我会回来处理这些空的catch子句的”,然后当然他们从未这样做。在这种情况下,受检异常实际上降低了大型系统的质量。
好的,我本来不想回复,但这个问题处理太长时间了,一方面有很多答案,另一方面我觉得有必要发表一下另一方面的看法。
我支持使用已检查异常——当它们被正确使用时——并认为它们是一件好事。我听到了上面所有的论点,对于一些反对使用已检查异常的论点,确实有一定的道理。但总体而言,我认为它们是积极的。我用过C#和Java两种编程语言,发现在C#中,程序更难以稳定地处理异常。使用已检查异常的最好之处在于,JavaDoc可以保证告诉你从该方法中可能抛出哪些异常。而在C#中,你需要依赖程序员记得告诉你任何一个给定方法可能抛出哪些异常,以及从该方法调用的任何方法可能抛出哪些异常等等。
如果你想创建五九可靠的代码,你需要知道从你调用的代码中可能抛出哪些异常,这样你就可以推断出什么可以恢复,什么必须让你放弃正在做的事情。在C#中,你可以做到这一点,但这需要大量的试错,直到你看到所有可能抛出的异常。或者你只是捕获异常并尽力而为。
Java和C#两种方法都有其优缺点。可以为Java和C#两种方法提出合理的论点,也可以反对它们。总之,我更喜欢Java所选择的方法,但如果我今天要重新编写Java,我会将一些已检查异常更改为运行时异常。Java API在使用已检查异常方面并不一致。正如其他人所说,Exception chaining作为标准API功能和JVM的一部分出现的时间太晚了。
然而,关于已检查异常的指责往往落入“懒惰的程序员滥用这个语言特性”的范畴。这是真的。但这也适用于许多语言及其特性。 “懒惰的程序员”论点是一个薄弱的论点。
让我们来看看那些不属于“懒惰的程序员”范畴的主要投诉:
可版本性 - 是的,在您代码的新版本中抛出一个新异常会打破那些盲目使用您的新JAR文件的客户端的编译。在我看来,这是一件好事情(只要您有一个抛出附加已检查异常的充分理由),因为您的库的客户必须推理出他们需要如何处理这种行为变化。如果所有异常都未经检查,那么您的客户不一定知道(直到发生异常时)您的行为已经发生了改变。如果您正在更改代码的行为,则您的客户需要知道这一点是合理的。您曾经更新过第三方库的新版本,结果发现它的行为已经不可见地改变了,现在您的程序已经无法运行?如果您对库进行破坏性行为的更改,那么您应该中断自动与使用您的库早期版本的客户机的兼容性。
可扩展性 - 如果您通过将其转换为适合于API层的特定已检查(或未经检查的)异常来正确处理已检查的异常,则此问题将成为非问题。也就是说,如果您编写得当,这个问题将消失。通过这样做,您适当地隐藏了您的实现细节,而您的调用者也不应该关心这些细节。
往往,这只是人们的宗教信仰问题,这就是我感到(不必要的,我知道)烦恼的原因。如果您对已检查异常有宗教上的厌恶,那没关系。如果您有合理的反对已检查异常的论点,那没有关系。我见过很多合理的论据(大部分我不同意,但还是……)。但是,我经常看到一些反对已检查异常的不好的论点,其中一些论点在Java 1.0时是公平和合理的,但在现代版本的Java中已经不适用了。
实践中最好使用已检查异常处理,因为当您的应用程序在凌晨2点开始记录错误日志并且您接到调试电话时,它可以提供更详细的信息...
在我看来,存在适合使用已检查异常的情况。在Java中可能有一些特性可以用不同的方式进行实现以更好地支持它们。这并非没有困难(例如,在某些情况下,您可能希望检查异常,在其他情况下则不需要)。当然,Java也支持未检查异常类型。
适合检查的异常类型通常应该进行文档记录。最好的记录位置是在代码中。通常只考虑成功的情况,这种民粹主义方法是错误的。
只要它们是可恢复的或不是由编程错误引起的,例如对 ResultSet 的无效索引访问,检查异常就非常好。否则,它们往往会通过强制程序员在许多方法签名中声明诸如 IOException 之类的内容来污染代码层和 API,并且对客户端代码没有真正有用的东西。
我认为在大多数情况下,检查异常都是浪费时间的。它们会陷入像Randolpho提到的反模式或广泛创建自定义异常以将实现与使用的库解耦等问题中。 摆脱这种“特性”可以让您专注于想要做的事情。