什么是异常?

4
我们每天都在谈论异常处理。我们都知道当执行遇到意外情况时会创建异常。
以下是几个问题:
  • 什么是异常?它在内存中的最底层组成是什么?在.NET中,我可以将其视为某种异常类型的对象实例。在本地环境中,它由什么构成?一些数据结构吗?

  • 如果异常没有被程序员显式抛出,那么是谁创建了这个异常呢?它是某种语言运行时提供的支持的一部分吗?

    SomeException e = new SomeException(); throw e;

  • 异常的工作范式是什么?当发生错误时,是否真的会创建对应数据结构/类型的实例来表示错误的详细信息?

  • 我们如何在运行时知道所有可能的意外情况,从而创建足够的异常数据结构/类型来表示它们?

感谢您的回复。

10
奇怪,你的名字里甚至有“维基百科”这个词。答案不在那里吗?http://en.wikipedia.org/wiki/Exception_handling 或者你是在研究这个主题以扩展文章? - Secure
2
这个问题是否真的只与C++和C#有关?它不适用于其他编程语言吗? - DerMike
抱歉,不允许使用太多的标签。 - smwikipedia
-1 试图引发争论 :-)。你的观点是什么?你是在准备面试问题,还是在为面试/测试学习?通过查看你的其他问题,我得出的印象是,你正在为 http://brainbench.com/ 这样的公司工作,对吗? :-) - Valentin H
还可以在这里看一下:http://www.simple-talk.com/dotnet/.net-framework/a-look-at-exceptions-in-.net-applications/ - sloth
7个回答

14

回答您的问题取决于所涉及的编程语言。

C并没有异常处理机制,尽管有一些专有语言扩展。
C++ 提供了在代码中的任意位置“抛出”任意对象并在调用堆栈的更高层次上“捕获”它的方式。
C#提供了“抛出”派生自System.Exception的对象以及从堆栈较高处“捕获”这些异常的方式。此外,我认为.NET运行时会报告由于抛出异常本身而引发的一些问题。

  • 什么是异常?它在内存中的最低级构成是什么?在.NET中,我可以将其视为某些异常类型的某个对象实例。在原生世界中,它由什么构成?一些数据结构吗?

在C++中,它只是一个像其他对象一样在代码中创建的任意对象:

throw 42;                     // throws an int object
throw "blah";                 // throws a char[5] object
throw std::string("arg!");    // throws a std::string object
throw my_type(42);            // throws a my_type object
throw std::exception("doh!"); // throws a std::exception object

抛出的异常与catch语句的匹配方式与重载函数的匹配方式非常相似。(一个很大的区别是catch语句是有序的。也就是说,第一个能匹配的catch语句将“获胜”并捕获对象。然而,重载函数必须始终提供明确的最佳匹配。)

在C++中,异常几乎只能从代码中抛出。它可能是您自己的代码、其他人的代码、某个库的代码或标准库的代码。但通常会在某个地方有一个 throw 语句。也有一些例外情况(不是故意的),比如由new引发的std::bad_alloc(它可能是从代码中的throw语句引发的,但我认为不一定需要),以及从 dynamic_cast<> 引发的std::bad_cast。(此外,下一个标准,C++1x,预计在明年发布,允许异常以某种方式跨越线程边界,这可能需要标准库实现者找到一种方法来在一个线程中存储异常并从另一个线程中重新引发它。但我对此还不太熟悉。)

如果异常在程序员的代码中没有被显式地抛出,那么是谁创建了这个异常?它是某些语言运行时提供的支持的一部分吗?

    SomeException e = new SomeException(); throw e;
在C++中,你可以抛出指针,但你很少这样做。通常情况下,你会使用 throw 关键字来抛出异常对象而不是指针。

In C++, throwing pointers can lead to memory leaks and undefined behavior if not properly handled.

SomeException e; throw e;
或者
throw SomeException();
  • 异常工作范式是什么?当发生错误时,语言运行时是否创建相应数据结构/类型的实例来表示错误详情?

除了C++标准库会抛出异常之外,我只能想到上面提到的两个特性(还有C++1x中的一个),即“运行时”(runtime)抛出异常时。

  • 如何知道所有可能在运行时产生的意外情况,并因此创建足够的异常数据结构/类型来代表它们?

在C++中,通常只抛出从std::exception派生的类对象,尽管我曾遇到过使用自己的异常类层次结构的代码,这些类没有根源于std::exception。标准库中异常层次结构的问题在于其类互相非虚拟继承,这使得不可能使用多重继承将自己的异常层次结构遮盖住标准库中的异常层次结构。(比如具有自己的OutOfRange异常类型,其从std::out_of_range和从您的异常基类MyException继承,MyException又从std::exception继承。)

C++处理异常的方法主要基于以下原则:

  • 以使其免于在任何点抛出异常的方式编写您的代码。使用RAII和其他技术来实现这一点。
  • 仅在可以对它们做出响应的位置捕获异常。
  • 只在非常规情况下抛出异常。不要将其用于控制流程。(C ++异常的设计目标是为了使供应商能够实现它们,从而在非异常情况下最小化开销,而以成本换取异常情况。)

我想你肯定不需要解释这个有什么问题吧?或者为什么要给它点踩? - sbi

1

在根本上,异常是一种具有预定义处理机制的特殊情况。其余所有内容取决于实现。

类型存在是因为方便在对象上存储有关特殊情况类型的额外数据。也就是说,异常可以是一个受控开关,例如每次硬件无法执行操作时只需重新启动机器。


1

异常实际上是一种错误处理方式,它会中断流程,使其无法继续。

异常有两个方面,抛出者和捕获者。抛出者负责检测错误是否发生,创建一个异常类来包含错误信息并将其“抛出”。这就像把它放在球里然后扔掉球一样——此时它的工作已经完成,现在轮到“捕获者”了。

捕获者是知道如何处理错误的错误处理机制。您可能会发现,捕获器只知道如何处理某些类型的错误。尽管在我的看法中,这应该不是使用运行时类型信息处理的最佳方式,因为这可能会导致开销。

通常情况下,在正常流程中使用异常是不可取的,例如当您读取文件直到结束时,文件结束并不是错误,而是预期发生的情况,应该使用正常流程而不是异常来处理。


0

异常是处理错误情况的一种方式。另一种选择是返回值,以及大量的if和switch/case语句。

虽然异常的确像是一个隐藏的goto语句,但最终的代码比它的替代方案更易于维护。

处理异常相当容易。在代码中抛出的所有异常都应该有一个共同的基类,用于记录错误。所有异常都应该携带足够的信息来说明发生了什么。

同样正确的是,异常不应该被滥用,以至于每个小事都要抛出异常。如果可能,应该在本地处理错误。如果不行,就抛出一个异常,在可以处理异常的级别上捕获它。

最后注意:没有异常的C++不是C++,而是带有类的C。


遗憾的是,根据他们的样式指南http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml?showone=Exceptions#Exceptions,谷歌不会同意你的最后一条评论。 - beldaz
@beldaz 我知道,但他们制定这样的编码规则可能有他们自己的原因。 - BЈовић

0
首先,我的回答仅适用于C++。C没有异常处理,而我对C#的了解不够深入,无法就该语言做出准确的陈述。
异常对象只是一个对象。它可以是某个类类型的对象,也可以很容易地是一个整数。在某种程度上,它类似于从函数返回的返回值。不同之处在于,由于异常处理所涉及的控制流问题,将异常存储在堆栈中会带来问题。
在C++中,所有的异常都始终是由throw语句引发的。在Microsoft SEH中,是语言运行时创建SEH异常。因此,总有一些代码会创建异常对象。
在C++中,语言运行时不必知道所有可能抛出的应用程序级别的异常。它只需要提供一个机制,使得catch处理程序可以访问(复制)被抛出的异常对象即可。这是由编译器编写者决定如何实现的。

0

0

传统的错误处理方法存在严重问题:

  • 从方法中返回错误代码需要将每个调用都包装在检查错误的if中。这本身就会使代码库几乎翻倍,如果你忘记做一次,你的程序就不再健壮。
  • 全局错误代码(如C语言中的errno)不是线程安全的。
  • 任何类型的错误代码只是一个数字。很难传达有关实际错误的更复杂细节。
  • 即使使用“异常结构”,也很难将它们组合起来 - 不同层次的代码可能使用不同的结构。
  • 等等...

异常允许您:

  • 传递错误信息的丰富性,因为您正在抛出实际对象而不仅仅是错误代码。对象的类型不必预先确定,并且可以在运行时“匹配”错误(实际上,C++中运行时类型识别(RTTI)的主要动机之一就是确定所抛出异常的运行时类型)。
  • 错误检测与错误处理分离。错误检测是在具有最多关于错误知识的代码片段中完成的(在调用层次结构的底部),而错误处理可以在代码的更高层次中完成,假定该层次了解有关用户界面的某些内容。
  • 介于之间的代码层不需要知道任何这些信息(但它们确实需要以“异常安全”的方式编写)。

什么是异常?
一个对象。
谁创建了异常...?
在 C++ 中,对象在被抛出之前被复制,因为编译器需要保证在堆栈展开期间异常的生命周期,只要有任何可以捕获它的 catch 块存在。
在 C# 中,由于其垃圾回收器,我们可以在堆栈展开期间保留原始对象。
异常工作范式是什么?
异常模拟异常事件。如果某些事情经常发生,则“可能”不是异常。
每种错误都可以用适当的不同类的实例来表示。或者您可以使用相同类来表示每个错误。或者介于两者之间。没有硬性规定。
我们如何在运行时知道所有可能的意外情况,从而创建足够的异常数据结构/类型来表示它们?
您知道“您”的代码可能会失败,因此可以适当地设计“您的异常”。在其他人编写的代码中,除非有证据证明否则假定它随时可能抛出任何异常。

出于好奇,您是否知道有哪些语言可以通过异常来解耦(1)触发它们的问题;(2)需要发生什么结果;以及(3)在代码可以正常继续之前需要解开多少事情?虽然这些因素是相关的,但它们也是不同的,特别是在可能存在多个问题的情况下。例如,将XML文档导入和合并到数据结构的操作可能会以一种使数据结构保持其原始状态、部分更新但完整或完全损坏的方式失败... - supercat
@supercat 异常强制进行某种范式变革 - 即使与异常无关的代码也必须假定它调用的每个子方法都可能抛出异常,除非证明不会抛出(这很少见)。然而,传统错误处理也必须执行类似的操作。事实上,它通常没有这样做并不意味着传统错误处理更容易,这只是意味着更容易被忽略,导致程序未能处理所有应该处理的错误。 - Branko Dimitrijevic
@supercat 到最后,使用异常的程序通常比使用传统错误处理的程序更加清晰和健壮。 - Branko Dimitrijevic
抛出异常的代码应该尽力使事物保持一致状态;但这并不意味着这样的努力总是会成功。例如,一个被认为从一个流复制数据到另一个流的方法,可能由于某些原因,在其中一个流遇到意外困难并且尝试清理另一个流的状态失败时,会导致一个或两个流处于不一致或无法使用的状态。代码应该回滚到一个不需要任何流有效的点,但我不知道任何正常模式可以做到这一点。 - supercat
@supercat 这就是我所说的“异常安全”代码。您应该始终假设任何代码行都可能抛出异常,并构造您的代码,使其不重要。在C++中,像RAII这样的习语或在C#中的IDisposable对于实现这个目标至关重要。在您的示例中,如果其中一个流失败,RAII保护或using也会自动“清理”另一个流。 - Branko Dimitrijevic
显示剩余10条评论

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