什么时候抛出异常?
我为我的应用程序不希望遇到的每种情况都创建了异常。 UserNameNotValidException,PasswordNotCorrectException等。
但是,我被告知我不应该为这些条件创建例外。在我的UML中,那些是主流的例外,那么为什么不应该例外呢?
有关创建例外情况的任何指导或者最佳做法?
解决方案
回答
异常在某种程度上造成了高昂的代价,例如,如果我们有一个用户提供了无效的密码,则通常最好将其传递回一个失败标志或者其他指示该标志无效的更好的主意。
这是由于异常的处理方式,真正的错误输入和唯一的紧急停止项应为异常,但不是失败的登录信息。
回答
因为它们是会正常发生的事情。异常不是控制流机制。用户经常会输入错误的密码,这不是例外。在" UserHasDiedAtKeyboard"类型的情况下,异常应该是很少见的事情。
回答
如果用户名无效或者密码不正确,也不例外。这些是我们在正常操作流程中应该期待的事情。异常不是正常程序操作的一部分,而且很少见。
编辑:我不喜欢使用异常,因为我们不能仅通过查看调用就知道方法是否引发异常。这就是为什么只有在我们不能以适当的方式处理这种情况(例如"内存不足"或者"计算机着火了")时才应使用异常的原因。
回答
我认为我们只能在无法采取任何措施摆脱当前状态时抛出异常。例如,如果我们正在分配内存而没有要分配的任何内存。在我们提到的情况下,我们可以清楚地从这些状态中恢复,并可以将错误代码相应地返回给调用方。
我们将看到很多建议,包括在回答此问题时,仅在"异常"情况下才应引发异常。从表面上看,这是合理的,但却是有缺陷的建议,因为它用另一个主观问题("什么是例外")代替了一个问题("我何时应该抛出例外")。相反,请遵循Herb Sutter的建议(对于C ++,可在Dobbs博士的文章《何时和如何使用异常》以及他与Andrei Alexandrescu的书中,C ++编码标准中找到):仅当且仅当抛出异常
- 不满足前提条件(通常使以下条件之一变为不可能)或者
- 替代方案将无法满足后置条件,或者
- 替代方案将无法保持不变。
为什么这样更好?它不是用关于前置条件,后置条件和不变式的几个问题代替问题吗?由于几个相关的原因,这样做更好。
- 前提条件,后置条件和不变式是我们程序(其内部API)的设计特征,而"抛出"的决定则是实现细节。它迫使我们记住,我们必须分开考虑设计及其实现,而在实现一种方法的同时,我们的工作就是生产出满足设计约束条件的产品。
- 它迫使我们根据前置条件,后置条件和不变量进行思考,这是我们的方法的调用者应该做出的唯一假设,并且要精确地表达它们,以使程序的各个组件之间能够松散耦合。
- 然后,这种松散的耦合使我们可以在必要时重构实现。
- 后置条件和不变式是可测试的;因为后置条件是我们的单元测试代码可以检查(断言)的谓词,所以它可以轻松地对单元代码进行测试。
- 根据后置条件进行思考自然会产生一个成功的设计,作为后置条件,这是使用异常的自然风格。程序的正常("快乐")执行路径是线性排列的,所有错误处理代码都移到了catch子句中。
回答
经验法则是在通常无法预测的情况下使用异常。例如数据库连接性,磁盘上丢失文件等。对于可以预测的情况,即,尝试使用错误密码登录的用户,应使用返回布尔值并知道如何妥善处理情况的函数。我们不希望仅仅因为有人输入了错误的密码而引发异常而突然终止执行。
回答
异常类就像"普通"类。当它是不同类型的对象,具有不同的字段和不同的操作时,可以创建一个新类。
根据经验,应该尝试在例外数量和例外粒度之间取得平衡。如果方法抛出超过4-5个不同的异常,则可以将其中一些合并为更多的"通用"异常(例如,在情况下为" AuthenticationFailedException"),然后使用异常消息详细说明出了什么问题。除非代码以不同的方式处理它们中的每一个,否则我们无需创建许多异常类。如果这样做的话,也许我们应该只返回一个包含所发生错误的枚举。这样比较干净。
回答
避免引发异常的主要原因是引发异常涉及很多开销。
以下文章指出,一件事是例外情况是由于特殊情况和错误引起的。
错误的用户名不一定是程序错误,而是用户错误...
这是.NET中异常的一个不错的起点:
http://msdn.microsoft.com/zh-CN/library/ms229030(VS.80).aspx
回答
通常,我们希望对应用程序中可能发生的"异常"事件抛出异常。
在示例中,这两个异常都看起来像我们是通过密码/用户名验证来调用它们的。在这种情况下,可以说有人输错用户名/密码并没有什么异常。
它们是UML主流的"例外",但在处理中更"分支"。
如果我们尝试访问passwd文件或者数据库,但无法访问,那将是一个例外情况,并且会引发异常。
回答
首先,如果API用户对特定的,细粒度的故障不感兴趣,那么为它们指定特定的异常就没有任何价值。
由于通常无法知道什么可能对用户有用,因此更好的方法是拥有特定的异常,但要确保它们从通用类继承(例如std :: exception或者C ++中的派生类)。这使客户可以选择特定的异常(如果他们选择的话),或者更普遍的异常(如果他们不在乎)。
回答
如果代码在循环中运行可能会一遍又一遍地导致异常,则抛出异常不是一件好事,因为它们对于大的N来说非常慢。但是,如果性能不佳,则抛出自定义异常也没有错。一个问题。只要确保我们拥有它们都继承的基本异常,即BaseException或者类似的东西。 BaseException继承了System.Exception,但是我们所有的异常都继承了BaseException。我们甚至可以使用异常类型树来对相似类型进行分组,但这可能会或者可能不会显得过大。
因此,简短的答案是,如果它不会导致明显的性能下降(除非抛出很多异常,否则不会出现),然后继续。
回答
对于这种情况,我们可以使用一些通用的例外。例如当方法的参数出现任何问题(ArgumentNullException除外)时,应使用ArgumentException。通常,我们不需要像LessThanZeroException,NotPrimeNumberException等异常。请考虑方法的用户。她将要专门处理的条件数量等于方法需要引发的异常类型的数量。这样,我们可以确定要处理的异常的详细程度。
顺便说一句,请始终尝试为库用户提供一些避免异常的方法。 TryParse是一个很好的例子,它存在,因此我们不必使用int.Parse并捕获异常。对于情况,我们可能想提供一些方法来检查用户名是否有效或者密码是否正确,以便用户(或者我们)将不必进行大量的异常处理。希望这将导致更多可读的代码和更好的性能。
回答
抛出异常会导致堆栈释放,这会对性能产生一些影响(公认的现代托管环境对此有所改善)。仍然在嵌套情况下反复抛出和捕获异常将是一个坏主意。
也许更重要的是,异常是针对特殊条件的。它们不应用于普通的控制流,因为这会损害代码的可读性。
回答
最终,决定权归于使用异常处理来处理应用程序级错误是否更有用,还是通过自己返回的状态代码(如返回状态码)来处理此类错误。我不认为有哪个更好的硬性规定,但我会考虑:
- 谁在打电话给代码?这是某种公共API还是内部库?
- 我们使用什么语言?例如,如果使用的是Java,则抛出(已检查)异常将使调用者以某种方式处理此错误情况,这显然是一种负担,而不是可以忽略的返回状态。那可能是好事也可能是坏事。
- 如何处理同一应用程序中的其他错误情况?调用者将不想处理以特殊方式处理错误的模块,这不同于系统中的任何其他模块。
- 所讨论的例程可能导致多少错误,如何处理它们?考虑处理不同错误的一系列catch块与错误代码的切换之间的区别。
- 我们是否有关于需要返回的错误的结构化信息?抛出异常为我们提供了比仅返回状态更好的放置此信息的位置。
回答
我的个人指导原则是:当发现当前代码块的基本假设为假时,将引发异常。
示例1:说我有一个函数,该函数应该检查一个任意类,如果该类从List <>继承,则返回true。该函数会询问以下问题:"此对象是List的后代吗?"此函数永远不会抛出异常,因为每个类中的每个操作类都不从List <>继承或者不从List <>继承,因此其操作中没有灰色区域,因此答案始终是"是"或者"否"。
示例2:说我有另一个函数,该函数检查List <>,如果其长度大于50,则返回true;如果长度小于50,则返回false。此功能会询问以下问题:"此列表是否包含50多个项目?"但是这个问题做出了一个假设,即假设给定的对象是一个列表。如果我将其设置为NULL,则该假设为假。在那种情况下,如果函数返回true或者false,则它违反了自己的规则。该函数无法返回任何内容并声称它已正确回答了问题。因此它不返回它会引发异常。
这可与"加载的问题"逻辑谬误相提并论。每个功能都会问一个问题。如果给出了输入,则该问题将成为谬论,然后引发异常。使用返回void的函数更难画这条线,但最重要的是:如果违反了函数关于其输入的假设,则应抛出异常而不是正常返回。
等式的另一面是:如果我们发现函数经常抛出异常,那么我们可能需要完善它们的假设。
回答
其他人则建议不要使用异常,因为如果用户键入错误,正常的登录流程将导致错误的登录。我不同意,也没有理由。将其与打开文件进行比较。.如果文件不存在或者由于某种原因不可用,则框架将引发异常。使用上面的逻辑,这是Microsoft的错误。他们应该返回了错误代码。解析,webrequests等都相同。
我不认为正常登录过程中的登录错误是很正常的。通常,用户键入正确的密码,并且该文件确实存在。例外情况是例外情况,对于这些情况使用例外情况是完全可以的。通过在堆栈的n层中传播返回值来使代码复杂化,这是浪费能量,并且会导致代码混乱。做可能可行的最简单的事情。不要通过使用错误代码来过早地进行优化,从定义上讲,很少发生异常的事情,并且除非我们抛出异常,否则异常不会花费任何代价。
回答
我遇到三种情况。
- 输入错误或者丢失也不例外。同时使用客户端js和服务器端regex来检测,设置属性并转发回带有消息的同一页面。
- AppException。通常这是我们在代码中检测到并抛出的异常。换句话说,这些是我们期望的(文件不存在)。记录它,设置消息,然后转发回常规错误页面。该页面通常会提供一些有关发生的情况的信息。
- 意外的异常。这些是我们所不知道的。记录详细信息并将其转发到常规错误页面。
希望这可以帮助
回答
异常适用于异常行为,错误,故障等事件。功能行为,用户错误等应改为由程序逻辑处理。由于错误的帐户或者密码是登录例程中逻辑流程的预期组成部分,因此它应该能够毫无例外地处理这些情况。
回答
异常与返回错误代码参数应该有关流控制而非哲学(错误的"异常"程度):
void f1() throws ExceptionType1, ExceptionType2 {} void catchFunction() { try{ while(someCondition){ try{ f1(); }catch(ExceptionType2 e2){ //do something, don't break the loop } } }catch(ExceptionType1 e1){ //break the loop, do something else }
}
回答
安全性与示例相混淆:我们不应告诉攻击者用户名存在,但密码错误。这是我们无需共享的额外信息。只需说"用户名或者密码不正确"。
回答
我会说,关于何时使用异常没有严格的规定。但是,有使用或者不使用它们的充分理由:
使用异常的原因:
- 常见情况的代码流更加清晰
- 可以将复杂的错误信息作为对象返回(尽管也可以使用引用传递的错误" out"参数来实现)
- 语言通常提供一些用于在发生异常时管理整洁的工具(在Java中尝试/最终在Java中使用,在C#中使用,在C ++中使用RAII)
- 如果没有抛出异常,则执行有时可能比检查返回码更快
- 在Java中,必须声明或者捕获已检查的异常(尽管这可能是反对的原因)
不使用例外的原因:
- 如果错误处理很简单,有时会过大
- 如果未记录或者声明异常,则调用代码可能无法捕获它们,这可能比调用代码只是忽略返回代码的情况更糟(应用程序退出vs静默失败-取决于情况)
- 在C ++中,使用异常的代码必须是异常安全的(即使我们不抛出或者捕获它们,而是间接调用抛出函数)
- 在C ++中,很难判断何时会抛出函数,因此,如果使用异常安全性,则必须对异常安全性抱有幻想
- 与检查返回标志相比,抛出和捕获异常通常要贵得多
通常,与C ++或者C#相比,我更倾向于在Java中使用异常,因为我认为已声明或者未声明的异常从根本上来说是函数形式接口的一部分,因为更改异常保证可能中断调用代码。在Java IMO中使用它们的最大优点是,我们知道调用者必须处理异常,这会增加正确行为的机会。
因此,在任何语言中,我总是会从通用类中在代码层或者API层中派生所有异常,以便调用代码始终可以保证捕获所有异常。我也认为在编写API或者库时抛出特定于实现的异常类是不好的(即包装较低层的异常,以便在接口的上下文中可以理解调用者收到的异常)。
请注意,Java区分了常规异常和运行时异常,因为后者无需声明。仅当我们知道错误是程序中的错误导致的时,我才使用运行时异常类。
回答
我同意japollock的看法-当我们不确定手术的结果时,请接受。调用API,访问文件系统,数据库调用等。每当我们越过编程语言的"边界"时。
我想补充一下,随意抛出一个标准异常。除非我们要进行"不同的"操作(忽略,电子邮件,日志,显示Twitter鲸鱼的图片等等),否则不要为自定义异常而烦恼。
回答
有两种主要的例外类别:
1)系统异常(例如,数据库连接丢失)或者
2)用户异常。 (例如,用户输入验证,"密码不正确")
我发现创建自己的用户异常类很有帮助,当我要抛出用户错误时,我想以不同的方式处理(即向用户显示资源错误),那么我在主要错误处理程序中要做的就是检查对象类型:
If TypeName(ex) = "UserException" Then Display(ex.message) Else DisplayError("An unexpected error has occured, contact your help desk") LogError(ex) End If
回答
在确定异常是否合适时需要考虑的一些有用的事情:
- 候选异常发生后,我们希望运行什么级别的代码-也就是说,调用栈应该展开多少层。通常,我们希望尽可能在发生异常的地方处理异常。对于用户名/密码验证,通常应在同一代码块中处理失败,而不是让异常冒出来。因此,异常可能不合适。 (OTOH,在三次失败的登录尝试之后,控制流可能会转移到其他位置,这里可能有例外。)
- 我们是否想在错误日志中看到此事件?并非每个异常都写入错误日志,但询问错误日志中的此项是否有用很有用-即,我们将尝试对此做某事,或者是我们忽略的垃圾。
回答
一个简单的答案是,每当不可能执行某项操作时(由于任一应用程序或者因为它可能违反业务逻辑)。如果调用了某个方法而无法执行编写该方法所要执行的操作,则抛出Exception。一个很好的例子是,如果无法使用提供的参数创建实例,则构造函数始终会引发ArgumentExceptions。另一个示例是InvalidOperationException,当由于另一个成员或者类成员的状态而无法执行操作时,抛出该异常。
在情况下,如果调用诸如Login(用户名,密码)之类的方法,则如果用户名无效,则抛出UserNameNotValidException确实是正确的;如果密码不正确,则抛出PasswordNotCorrectException。用户无法使用提供的参数登录(即不可能,因为它会违反身份验证),因此请抛出Exception。虽然我可能让两个Exception从ArgumentException继承。
话虽如此,如果我们不希望因为登录失败很常见而抛出异常,一种策略是改为创建一种返回代表不同失败类型的方法。这是一个例子:
{ // class ... public LoginResult Login(string user, string password) { if (IsInvalidUser(user)) { return new UserInvalidLoginResult(user); } else if (IsInvalidPassword(user, password)) { return new PasswordInvalidLoginResult(user, password); } else { return new SuccessfulLoginResult(); } } ... } public abstract class LoginResult { public readonly string Message; protected LoginResult(string message) { this.Message = message; } } public class SuccessfulLoginResult : LoginResult { public SucccessfulLogin(string user) : base(string.Format("Login for user '{0}' was successful.", user)) { } } public class UserInvalidLoginResult : LoginResult { public UserInvalidLoginResult(string user) : base(string.Format("The username '{0}' is invalid.", user)) { } } public class PasswordInvalidLoginResult : LoginResult { public PasswordInvalidLoginResult(string password, string user) : base(string.Format("The password '{0}' for username '{0}' is invalid.", password, user)) { } }
大多数开发人员都被告知要避免抛出异常,因为抛出异常会导致开销。精打细算的资源很棒,但通常不会以牺牲应用程序设计为代价。这可能是我们被告知不要抛出两个异常的原因。是否使用异常通常可以归结为异常发生的频率。如果这是一个相当普遍或者可预期的结果,那么这是大多数开发人员会避免使用Exception,而是由于假定的资源消耗而创建另一个方法来指示失败的时候。
这是一个使用Try()模式避免在上述场景中使用Exception的示例:
public class ValidatedLogin { public readonly string User; public readonly string Password; public ValidatedLogin(string user, string password) { if (IsInvalidUser(user)) { throw new UserInvalidException(user); } else if (IsInvalidPassword(user, password)) { throw new PasswordInvalidException(password); } this.User = user; this.Password = password; } public static bool TryCreate(string user, string password, out ValidatedLogin validatedLogin) { if (IsInvalidUser(user) || IsInvalidPassword(user, password)) { return false; } validatedLogin = new ValidatedLogin(user, password); return true; } }
回答
我要说的是,通常每一种原教旨主义都会导致地狱。
我们当然不想以异常驱动流结束,但是完全避免异常也是一个坏主意。我们必须在两种方法之间找到平衡。我不会为每种异常情况创建一个异常类型。那没有生产力。
我通常更喜欢创建在整个系统中使用的两种基本类型的异常:LogicalException和TechnicalException。如果需要,可以通过子类型进一步区分这些类型,但是通常没有必要。
技术异常表示真正出乎意料的异常,例如数据库服务器关闭,与Web服务的连接引发IOException等。
另一方面,逻辑异常用于将不太严重的错误情况传播到上层(通常是一些验证结果)。
请注意,即使是逻辑异常,也不打算定期使用它来控制程序流程,而是要强调流程真正结束的情况。在Java中使用时,两种异常类型都是RuntimeException子类,并且错误处理高度面向方面。
因此,在登录示例中,创建诸如AuthenticationException之类的内容并通过诸如UsernameNotExisting,PasswordMismatch之类的枚举值来区分具体情况可能是明智的。然后,我们将不会陷入巨大的异常层次结构,并且可以将catch块保持在可维护的水平。我们还可以轻松地采用某种通用的异常处理机制,因为我们已对异常进行了分类,并且非常了解向用户传播的内容以及传播方式。
我们的典型用法是在用户输入无效时在Web Service调用期间引发LogicalException。异常被编组到SOAPFault详细信息,然后再次在客户端上被编组到异常,这导致在一个特定的网页输入字段上显示验证错误,因为该异常已正确地映射到该字段。
这当然不是唯一的情况:我们无需点击Web服务即可引发异常。在任何特殊情况下(例如需要快速失败的情况),我们都可以自由选择这样做。
回答
我的一些小指导方针深受伟大的"代码完成"书的影响:
- 使用异常通知不应该忽略的事情。
- 如果错误可以在本地处理,请不要使用异常
- 确保异常与例程的其余部分处于相同的抽象级别。
- 对于真正例外的情况,应保留例外。
回答
我在使用例外方面有哲学上的问题。基本上,我们期望会发生特定的情况,但是我们没有明确地处理它,而是将问题推到了"别处"。任何人都可以猜到"其他地方"在哪里。
回答
当我们想在返回值中表示两个或者更多个错误状态时,请创建新的异常。
回答
在我看来,最基本的问题应该是,如果发生某种情况,是否可以期望调用者继续正常的程序流程。如果我们不知道,则可以使用单独的doSomething和trySomething方法(前者返回错误而后者不返回错误),或者有一个接受参数的例程来指示如果失败则应引发异常。考虑一个类,用于将命令发送到远程系统并报告响应。某些命令(例如重新启动)将导致远程系统发送响应,但在一定时间内无响应。因此,能够发送" ping"命令并找出远程系统是否在合理的时间长度内响应而又不必抛出异常(如果调用者不希望,则前者可能希望前几个" ping"尝试将失败,但最终会成功)。另一方面,如果有一系列命令,例如:
exchange_command("open tempfile"); exchange_command("write tempfile data {whatever}"); exchange_command("write tempfile data {whatever}"); exchange_command("write tempfile data {whatever}"); exchange_command("write tempfile data {whatever}"); exchange_command("close tempfile"); exchange_command("copy tempfile to realfile");
人们会希望任何操作失败都会中止整个序列。虽然可以检查每个操作以确保操作成功,但如果命令失败,则让exchange_command()例程抛出异常会更有用。
实际上,在上述情况下,具有一个参数来选择多种故障处理模式可能会有所帮助:从不抛出异常,仅针对通信错误抛出异常,或者在命令未返回"成功"的任何情况下抛出异常"指示。