什么是“exceptional”?

11
我想了解在单元测试或面向对象编程原则方面,“异常情况”(exceptional)的定义。在 Stack Overflow 上讨论抛出异常时,有几次看到这样的评论:“我不认为从 Bar 获取 Foo 是个例外情况。”(是否有 Trollface 表情符号?)
我进行了一些谷歌搜索,但没有立即找到答案。是否有任何好的定义、经验法则或指南比意见或偏好更加客观?如果一个人说“这是例外情况”,而另一个人说“不,不是”,如何解决这个争议?
编辑:哇,已经有很多答案了,有人持不同意见,并且它们似乎都是回答者的观点 :P 没有人引用维基百科页面或一位资深程序员的文章。我从中得出的“元答案”是并没有对于何时使用异常情况达成一致的经验法则。因此,我感到更有信心在选择抛出异常时采用自己个人的特殊规则。

7
根据我所见,拥有单元测试相当罕见! - Ken
我看不出这与单元测试有什么关系。 - Michael Borgwardt
2
@Ken:拥有它们很容易。但要让它们保持最新、全面和相关性,就不那么容易了... - Steven Sudit
你使用的是哪种编程语言?因为你使用了“exceptional”这个词,所以听起来像是在问异常(Exceptions)方面的问题。如果你是在使用英语中的普通意义,那么更清楚的说法应该是“expectations”,因为单元测试就是关于什么可以预期和在正常使用情况下超出预期范围的内容。我期望两个整数相加的结果返回一个整数。如果我得到了一个虚数,那就超出了我的预期。 - MatthewMartin
我用了“exceptional”这个词,但我不知道它的意思。我只是因为其他人在SO上使用过它(“我不认为情况X是exceptional”(意思是你不应该在这里抛出异常)),所以我必须使用这个词来询问它的定义。 - user151841
这个问题的标题选择非常糟糕。 - bmargulies
13个回答

8

我不同意其他答案关于某些情况需要“特殊”才能引起“异常”的说法。我认为,“特殊”是指任何时候你想通过抛出一个向上传播的对象来打破正常的控制流。

异常是一种语言特性,是达到目的的手段。它们并不神奇。像其它特性一样,使用它们应该根据你需要它们提供的东西的时间来指导,而不是根据它们的名称。


异常并不总是语言特性。例如,在Windows下的C中,结构化异常处理基本上是一种操作系统功能。 - Steven Sudit
@Steven Sudit:确实如此,但是如果没有语言支持来抛出或捕获它们,它们就不是语言的一部分,因此实际上并不相关。SEH只有在MSVC支持__try/__catch时才真正可能。 - Puppy
@DeadMG:有__try/_catch是一件好事,但即使缺少这个功能,SEH风格的异常仍然会被抛出。实际上,即使程序是用汇编手工制作的,它们也会被抛出。这就是我所说的操作系统特性;语言依赖于它,但它并不依赖于语言。 - Steven Sudit
@DeadMG:也许我没有表达清楚。任何语言都不需要特定的支持来抛出SEH异常。一个简单的除零或无效指针都会生成异常,无论编译器如何。只有在我们想要处理异常而不是让它终止进程时才需要支持。 - Steven Sudit
1
@Steven:我认为这取决于情况。如果你从文件中读取行直到达到EOF,那么这不是异常情况(实际上也不是错误,只是终止条件)。如果你尝试从文件中读取512字节的块,在读取512字节之前就到达EOF,我认为这是异常情况。 - James McNellis
显示剩余7条评论

5

如果满足以下条件,那么这是一个例外情况:

  1. 它是一个失败条件。 并且

  2. 它偶尔而又出乎意料地发生。 并且

  3. 没有更好的机制来报告它。

编辑

从Dan Weinreb的博客文章中抄袭,Ken在这里发布了相关内容,我想提供以下关于异常的总结。

  1. 方法的契约定义了如何(以及是否)信号化异常条件(即故障)。如果该方法表示某个东西是异常情况,那么它就是异常情况。当然,这还留下了如何设计契约的问题。

  2. 异常的好处在于不需要调用者进行任何检查,并自然地上升,直到被能够处理它们的东西捕获。它们还可以包含重要的详细信息、用户可读的文本和堆栈跟踪。这些特性使它们非常适合防止进一步处理但不可预测或常见的故障情况,或者在明确的错误处理会对代码流产生干扰的情况下。它们特别适用于“永远不应该发生”的错误,但其影响是灾难性的(例如堆栈溢出)。

  3. 标志、错误代码、魔术值(NULL、nil、INVALID_HANDLE等)和其他基于返回的机制不会劫持流程,因此更适合于常见且最好在本地处理的情况,特别是那些可以通过工作来解决失败的情况。由于它们是按照惯例而不是法令操作的,所以您不能指望它们被检测和处理,除非实际使用时无效值可能被设计为引发异常(例如使用INVALID_HANDLE进行读取)。

  4. 在健壮的代码中使用异常时,每个方法都应捕获意外异常并将它们包装在契约异常中。换句话说,如果您的方法不承诺抛出NullReferenceException,您需要捕获它并在更通用或更具体的异常中重新抛出它。它们被称为异常,而不是惊喜!


是使用 1 && 2 && 3 还是 1 || 2 || 3? - Mchl
@Mchl:抱歉,我表达不清楚。是的,这是一个AND操作符。必须满足以上所有条件。 - Steven Sudit
这听起来像是不错的建议,但我能在其他地方听到这个建议吗?我正在寻找被广泛接受的准则。 - user151841
@Steven 我会的。(我没有给你投反对票)。我只是在寻找一些关于异常处理方面“权威”的东西 - 在SO上,我看到了很多相互矛盾的意见。说实话,“我不认为这是‘特殊情况’”真的让我很生气 :)。我希望人们在推进自己的个人偏好时能够直截了当,否则就引用一些被广泛接受的原则,而不仅仅将其陈述为“应该这样做的方式”。 - user151841
@user:希望这样可以让您将“I don't consider EOF to be exceptional”转变为“EOF不是异常,因为它很常见且可预期”。 - Steven Sudit
显示剩余8条评论

2

一个通用的经验法则是:

  • 对于您预料到可能发生的错误,请使用异常处理
  • 对于永远不可能发生的错误,请使用断言处理

我认为OP并不是将异常与断言进行对比,而是与其他错误处理方法进行对比,例如返回“false”。 - Steven Sudit
在我看来,这是一个非常糟糕的经验法则。异常只是另一种具有某些优缺点的流程控制机制。它是否是正确的选择不应该由其名称来确定。 - Michael Borgwardt
断言是关于仅在调试时进行的运行时检查,会导致立即终止。它们是一种测试形式,而不是任何用于生产中的错误处理形式。 - Steven Sudit
@Steven:我从OP的问题中推断出他不确定何时使用异常,这在他的编辑中得到了确认。此外,我的观点是,只要断言不会影响性能,就应该将其保留在生产代码中。如果我没记错的话,《代码大全》建议不要让它们显示消息,而是将它们写入日志文件。 - Nobody
如果一个测试包含在发布代码中,我就不再称其为断言。并不是说这一做法一定是不好的,只是我们需要一个不同的标签来区分它与仅用于调试的测试。 - Steven Sudit

1

在我看来,如果程序的进一步执行会导致致命错误或不可预测的行为,应该抛出异常。


根据这个,读取文件时遇到EOF是否属于异常情况? - Steven Sudit
@Steven,如果您没有预料到文件的结束,那么是的...这是一个异常情况。 - Matthew Whited
@Matt:据我所知,文件总是有一个结尾,因此当我们完全读取它们时,我们应该期望EOF。你可以建议在读取之前检查长度,这不是一个坏主意,但是真实的文件在这样的检查后可能会被其他进程修改。这就是为什么非异常路径是简单地返回实际读取的字节数,当我们到达EOF时,这个字节数为0。 - Steven Sudit
@Steven,假设记录是固定长度的,因此可以假定长度为该长度的倍数。如果在读取过程中到达文件结尾,则会抛出异常。此异常将指示文件读取失败......此时您没有足够的信息来知道原因。可能是网络驱动器崩溃了,或者最后一次保存时文件损坏了。甚至可能是文件未正确锁定,另一个应用程序移动了分配指针而您的应用程序没有预期到这种情况。 - Matthew Whited
你的意思是File.Exists()会抛出异常,还是你会根据File.Exists()的返回值来手动抛出异常...无论哪种情况,都要符合特定情境的预期。 - Matthew Whited
显示剩余6条评论

1

就我个人而言,我认为这种讨论纯粹浪费时间,“什么是异常”是错误的问题。

异常只是另一种流程控制机制,具有某些优点(控制流可以通过调用堆栈向上多级传递)和缺点(有点冗长,行为不太局部化)。

它是否是正确的选择不应该由其名称来确定。如果异常被简单地称为“bubbleups”,我们会进行这些讨论吗?


我想在这个意义上,这更多是一个“如何与其他程序员合作的问题?”(特别是在“你不应该在这里抛出异常,因为这种情况并不是异常”这种情况下)。我们不会对bubbleups进行这样的讨论 :) - user151841
我们将会询问这个错误是否值得冒泡,但是潜在的问题仍然存在。异常处理的重要之处在于,与返回代码不同,无需检查异常。"在苏联,返回代码检查你!" - Steven Sudit
@user151841 @Steven:区别在于,“它是否特殊?”的讨论风险更多地关注“特殊”一词,而忽略了实际的设计问题以及这种流程控制如何帮助解决它。 - Michael Borgwardt

1
当有人拔掉电源线时,那是特殊情况,大多数其他情况都是预期的。

1
发型硬的异常是最好的异常。 - Tom
好的,当我将一个 object 强制转换为 int 时,如果它实际上是一个 string,那么我应该怎么做,而不是抛出异常? - Steven Sudit
1
据我所知,愚蠢的行为也算是异常情况 :) 也许这就是“健全性检查”一词的由来。 - leppie
1
愚蠢是很寻常的。如果有什么的话,那就是预料之中的事情。 - Steven Sudit
@Steven Sudit:我们不是在说同样的事情吗?当然,抛出异常是“处理它”的一种方式(也可能是首选方式)。 - leppie
我们可能会这样认为。我理解你的回答是除了硬件故障之外,其他任何异常都是可以预料的,这意味着我们不应该抛出异常。显然,我误解了。 - Steven Sudit

1

嗯,异常编程案例是指偏离正常流程的程序情况,可能是由于以下原因:

::硬件或软件故障,程序员在给定情况下无法处理。程序员可能不确定在这些情况下该怎么做,因此将其留给用户或调用此代码的工具/库。 ::即使程序员可能不确定他的代码将在哪种环境中使用,因此最好将错误处理留给使用代码的人。

因此,程序的异常情况可能是在不常见的环境或与不常见的交互中使用它。 再次,不常见的交互和未知的环境是从设计师的角度来看的。 因此,从正常情况偏离是异常的,而且它基于程序员的观点和上下文。

这样说是否有点绕?:D


1

异常在问题的直接范围之外出现时非常有用。它们几乎永远不应该在抛出异常的地方被捕获,因为如果可以在那里满意地处理它们,就可以在不抛出任何东西的情况下满意地处理它们。

一个例子是C++的容器,如果它们无法获得所需的内存,就会抛出bad_alloc。编写容器的人不知道如果容器无法获得内存应该发生什么。也许这是一件预期的事情,调用代码有替代方案。也许这是可恢复的。也许这是致命的,但应该如何记录?

是的,可以返回错误代码,但它们会被使用吗?我看到很多C内存分配没有测试NULL,并且printf只是丢弃返回值。此外,许多函数没有可区分的错误代码,例如printf为负数和内存分配为NULL。在任何返回值都可能有效的情况下,需要找到一种返回错误指示的方法,这比大多数程序员愿意处理的更加复杂。异常不能被忽略,并且不需要大量的防御性代码。


我不确定关于非本地处理的部分是否完全准确。当然,异常具有一直上升直到被捕获的优点,但有时捕获它们的正确位置是在它们发生时。例如,如果我有一个弹出文件选择对话框并加载文件的方法,那么这是指示用户其文件选择无效并要求他们选择另一个或放弃的理想位置。 - Steven Sudit
如果捕捉它们的正确位置是在程序运行时,考虑能否在代码中方便地捕捉错误。这不是总是可行的方法,但更有可能成功。 - David Thornley

1

我看过的最好的讨论是Dan Weinreb的博客:什么是真正的条件(异常)

这个话题表面上是关于Common Lisp的,它的条件系统类似于更灵活的异常形式,但几乎没有Lisp代码,而且你不需要成为一个Common Lisp程序员就能理解这些概念。


有趣的文章。我将毫不客气地从中借鉴,并添加到我的答案中。 - Steven Sudit

0
另一个好的经验法则是 - 永远不要使用异常来捕获您可以在代码中捕获的条件。
例如,如果您有一个方法来除以2个变量,请勿使用异常来捕获除零错误,而是事先进行必要的检查。
糟糕的代码示例:
float x;
try {
x = a / b;
{
catch (System.DivideByZeoException) {
x = 0;
}

良好的代码示例:

if (b == 0)
return 0;

异常通常是很昂贵的。


代码的意图非常重要。如果你只是针对无效输入返回一个有效值,那么以后可能会遇到很大的问题。 - Matthew Whited
1
a/0并不意味着零,从数学上讲是没有意义的。它很可能意味着用户输入错误,或者如果b是机器生成的,则b是由有缺陷的过程生成的。 - MatthewMartin
强调一下Matthew所说的,你在示例中误用了异常。如果b为零,那么可能出了什么问题,返回默认值只会掩盖问题,让你陷入更深的困境。catch的正确位置应该是在某个地方,你可以半智能地处理这个问题。此外,像你第二个示例中的防御性编码可能会有成本,如果几乎不会抛出异常,那么依赖它可能是划算的。 - David Thornley
我要补充的是,仅检查b是否为零是不够的。当a很大而b很小时,我们仍可能溢出。 - Steven Sudit
1
大家的评论都很中肯,我的例子可能没有想得太周全。然而,在这个例子中,b为零可能是微不足道和常见的情况,我曾经看到过一些代码捕获divbyzeroexception并返回0,这是昂贵且不必要的。 - Matt Roberts
同意:即使我们需要为由于大小极端而引起的溢出设置一个catch,检查失败的明显原因——零分母——并尽早失败也不是不合理的。 - Steven Sudit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接