何时抛出异常?

我为我的应用程序不期望的每个条件创建了异常。UserNameNotValidExceptionPasswordNotCorrectException等。

然而,我被告知我不应该为这些条件创造例外。在我的UML中,那些是主要流程的异常,那么为什么它不应该是异常呢?

是否有创建异常的指导或最佳实践?

173870 次浏览

异常是一种代价高昂的效果,例如,如果您有一个用户提供了无效的密码,那么通常更好的方法是返回一个失败标志,或其他一些无效的指示。

这是由于异常处理的方式,真正的错误输入和唯一的关键停止项应该是异常,而不是失败的登录信息。

因为它们是正常发生的事情。异常不是控制流机制。用户经常会输入错误的密码,这不是特例。异常应该是一个真正罕见的事情,UserHasDiedAtKeyboard类型的情况。

如果用户名无效或密码不正确,这不是一个例外。这些都是在正常操作流程中应该预料到的事情。异常不属于正常程序操作的一部分,而且相当罕见。

我不喜欢使用异常,因为仅仅通过查看调用就无法判断一个方法是否引发了异常。这就是为什么只有当你不能以一种体面的方式处理这种情况时才应该使用异常(想想“超出记忆”)。或者“电脑着火了”;)。

我认为只有在无法摆脱当前状态时才应该抛出异常。例如,如果您正在分配内存,但没有任何内存可以分配。在您提到的情况下,您可以清楚地从这些状态中恢复,并相应地将错误代码返回给调用者。


您将看到许多建议,包括对这个问题的回答,即您应该只在“异常”情况下抛出异常。这看似合理,但却是有缺陷的建议,因为它用另一个主观问题(“什么是异常”)取代了一个问题(“我应该在什么时候抛出异常”)。相反,请遵循Herb Sutter的建议(对于c++,可在Dobbs博士文章何时以及如何使用例外中找到,也可在他与Andrei Alexandrescu合著的书c++编码标准中找到):当且仅当抛出异常

  • 表示未满足前提条件(通常会出现以下情况之一 李不可能)或< / >
  • 替代方案将无法满足后置条件或
  • 替代方案将无法保持不变式。

为什么这样更好呢?它不是用关于前置条件、后置条件和不变量的几个问题替换了问题吗?这是更好的几个相关的原因。

  • 前置条件、后置条件和不变量是程序(其内部API)的设计特征,而决定throw是实现细节。它迫使我们记住,我们必须分别考虑设计和实现,在实现方法时,我们的工作是产生满足设计约束的东西。
  • 它迫使我们考虑前置条件、后置条件和不变量,这些都是方法的调用者应该做出的只有假设,并且被精确地表达出来,从而实现程序组件之间的松耦合。
  • 这种松耦合允许我们在必要时重构实现。
  • 后置条件和不变量是可测试的;它产生的代码可以很容易地进行单元测试,因为后置条件是单元测试代码可以检查(断言)的谓词。
  • 从后置条件的角度考虑自然会产生具有成功是后置条件的设计,这是使用异常的自然风格。程序的正常(“愉快”)执行路径是线性布局的,所有错误处理代码都移动到catch子句中。

一个经验法则是在您通常无法预测的情况下使用异常。例如数据库连接、磁盘上丢失的文件等。对于您可以预测的场景,例如用户试图使用错误的密码登录,您应该使用返回布尔值的函数,并知道如何优雅地处理这种情况。您不希望仅仅因为有人输入了密码错误而抛出异常,从而突然结束执行。

异常类就像“正常”类。当一个新类“是”一个不同类型的对象,具有不同的字段和不同的操作时,您可以创建一个新类。

根据经验,您应该尝试在异常数量和异常粒度之间取得平衡。如果你的方法抛出超过4-5个不同的异常,你可以将其中一些合并为更“通用”的异常(例如,在你的例子中是“AuthenticationFailedException”),并使用异常消息详细说明哪里出了问题。除非您的代码以不同的方式处理它们,否则您不需要创建许多异常类。如果发生了,你应该返回一个枚举,其中包含发生的错误。这样比较干净。

避免抛出异常的主要原因是抛出异常涉及大量开销。

下面这篇文章指出的一件事是,例外是针对异常条件和错误的。

错误的用户名不一定是程序错误,而是用户错误……

下面是。net中异常的一个不错的起点: http://msdn.microsoft.com/en-us/library/ms229030 (VS.80) . aspx < / p >

一般来说,你想要为应用程序中可能发生的任何异常抛出一个"异常"

在您的示例中,这两个异常看起来都是通过密码/用户名验证调用的。在这种情况下,有人会输入错误的用户名/密码并不是什么例外。

它们是UML主要流程的“例外”,但在处理过程中是更多的“分支”。

如果您试图访问您的passwd文件或数据库,但无法访问,这将是一个异常情况,并需要抛出异常。

首先,如果API的用户对特定的、细粒度的故障不感兴趣,那么为他们设置特定的异常就没有任何价值。

由于通常不可能知道什么可能对用户有用,一个更好的方法是有特定的异常,但确保它们继承自一个公共类(例如,std::exception或其在c++中的派生类)。这允许您的客户端捕获特定的异常(如果他们愿意的话),或者捕获更一般的异常(如果他们不关心的话)。

如果在循环中运行的代码可能会反复导致异常,那么抛出异常就不是一件好事,因为对于大n来说,抛出异常非常慢。但如果性能不是问题,抛出自定义异常也没有什么问题。只要确保你有一个它们都继承的基异常,叫做BaseException之类的。BaseException继承System。异常,但是你所有的异常都继承了BaseException。您甚至可以有一个异常类型树来对相似的类型进行分组,但这可能会过度,也可能不会。

因此,简短的回答是,如果它不会导致显著的性能损失(除非抛出大量异常,否则不应该如此),那么就继续执行。

你可以在这种情况下使用一些通用的异常。例如,ArgumentException用于当方法的参数出现任何错误时(ArgumentNullException除外)。通常你不需要像LessThanZeroException, NotPrimeNumberException等异常。想想你的方法的用户。她想要特别处理的条件的数量等于你的方法需要抛出的异常类型的数量。通过这种方式,您可以确定将有多么详细的异常。

顺便说一下,总是尝试为库的用户提供一些避免异常的方法。TryParse就是一个很好的例子,它的存在使你不必使用int。解析并捕获异常。在您的情况下,您可能希望提供一些方法来检查用户名是否有效或密码是否正确,这样您的用户(或您)就不必进行大量异常处理。这将有望产生更易于阅读的代码和更好的性能。

抛出异常会导致堆栈unwind,这对性能有一定影响(承认,现代托管环境在这方面有所改进)。仍然在嵌套的情况下反复抛出和捕获异常是一个坏主意。

可能比这更重要的是,例外是针对特殊情况的。它们不应该用于普通的控制流,因为这会损害代码的可读性。

最终,决定取决于是使用异常处理更有助于处理此类应用程序级错误,还是通过您自己的机制(如返回状态代码)更有帮助。我不认为哪个更好有一个严格的规则,但我会考虑:

  • 谁在调用你的代码?这是某种公共API还是内部库?
  • 你在用什么语言?例如,如果它是Java,那么抛出(已检查)异常会显式地加重调用者以某种方式处理此错误条件的负担,而不是可以忽略的返回状态。这可能是好事,也可能是坏事。
  • 如何处理同一应用程序中的其他错误条件?调用者不希望处理一个以特殊方式处理错误的模块,这种方式与系统中的其他任何模块都不同。
  • 有多少事情会出问题的例行程序,他们将如何处理不同?考虑处理不同错误的一系列catch块和错误代码开关之间的区别。
  • 您是否有关于需要返回的错误的结构化信息?抛出异常为您提供了一个更好的位置来放置这些信息,而不仅仅是返回一个状态。

我个人的指导方针是:当发现当前代码块的基本假设为假时抛出异常。

例1:假设我有一个函数,它应该检查一个任意的类,如果这个类继承了List<>,则返回true。这个函数问一个问题:“这个对象是List的后代吗?”这个函数永远不会抛出异常,因为它的操作中没有灰色地带——每个单独的类要么继承List<>,要么不继承,所以答案总是“是”或“否”。

例2:假设我有另一个函数,它检查List<>,如果它的长度大于50则返回true,如果长度小于50则返回false。这个函数问一个问题,“这个列表是否有超过50个条目?”但是这个问题做了一个假设——它假设给出的对象是一个列表。如果我给它一个NULL,那么这个假设是错误的。在这种情况下,如果函数返回要么 true false,则它违反了自己的规则。函数不能返回任何东西并声称它正确地回答了问题。所以它不返回,而是抛出一个异常。

这与“加载问题”逻辑谬误类似。每个函数都问一个问题。如果给出的输入使该问题成为谬误,则抛出异常。对于返回void的函数,这条线很难画出来,但底线是:如果函数对其输入的假设违反了,它应该抛出异常,而不是正常返回。

这个等式的另一方面是:如果你发现你的函数经常抛出异常,那么你可能需要改进它们的假设。

其他人建议不应该使用异常,因为在正常流程中,如果用户输入错误,就会出现错误的登录。我不同意,我不明白其中的道理。与打开文件相比。如果该文件不存在或由于某种原因不可用,则框架将抛出异常。使用上述逻辑是微软的一个错误。他们应该返回一个错误代码。解析、webrequest等也一样。

我不认为糟糕的登录是正常流程的一部分,这是例外。通常情况下,用户输入正确的密码,文件就存在了。例外的情况是例外的,对于这些情况使用例外是完全没问题的。通过将返回值传播到堆栈上的n层来使代码复杂化是一种浪费精力的行为,并将导致混乱的代码。做最简单的事情。不要过早地使用错误代码进行优化,根据定义,异常很少发生,除非抛出异常,否则异常不会造成任何损失。

我有三种情况。

  1. 错误或丢失的输入不应该是例外。同时使用客户端js和服务器端regex来检测、设置属性并转发回带有消息的同一页面。

  2. AppException。这通常是您在代码中检测和抛出的异常。换句话说,这些是您所期望的文件(该文件不存在)。记录它,设置消息,并转发回常规错误页面。这个页面通常有一些关于所发生的事情的信息。

  3. 意外的异常。这些是你不知道的。记录详细信息,并将其转发到一般错误页面。

希望这能有所帮助

异常用于异常行为、错误、失败等事件。功能行为、用户错误等应该由程序逻辑来处理。由于错误的帐户或密码是登录例程中逻辑流的一部分,因此它应该能够毫无例外地处理这些情况。

异常与返回错误代码参数应该是关于流控制的,而不是哲学(错误有多“异常”):

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中的try/finally, c#中的使用,c++中的RAII)
  • 在没有抛出异常的情况下,执行有时可能比检查返回代码快
  • 在Java中,必须声明或捕获已检查的异常(尽管这可能是反对的原因)

不使用异常的原因:

  • 有时,如果错误处理很简单,那就太过分了
  • 如果没有记录或声明异常,则调用代码可能无法捕获异常,这可能比调用代码忽略返回代码更糟糕(应用程序退出与静默失败——后者可能更糟糕,这取决于场景)
  • 在c++中,使用异常的代码必须是异常安全的(即使你不抛出或捕获它们,而是间接调用抛出函数)
  • 在c++中,很难判断一个函数什么时候可能会抛出,因此如果你使用它们,你一定对异常安全很偏执
  • 与检查返回标志相比,抛出和捕获异常的代价通常要高得多

一般来说,我更倾向于在Java中使用异常,而不是在c++或c#中,因为我认为异常,无论是否声明,都是函数正式接口的基本组成部分,因为更改异常保证可能会破坏调用代码。在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

在决定异常是否合适时,需要考虑一些有用的事情:

  1. 您希望在候选异常发生后运行什么级别的代码——也就是说,应该展开调用堆栈的多少层。您通常希望处理异常时尽可能靠近异常发生的地方。对于用户名/密码验证,通常会在同一代码块中处理失败,而不是让异常冒出来。所以一个例外可能是不合适的。(OTOH,在三次失败的登录尝试后,控制流可能会转移到其他地方,这里可能会出现异常。)

  2. 您希望在错误日志中看到这个事件吗?并不是每个异常都被写入错误日志,但是询问错误日志中的这个条目是否有用是有用的——也就是说,您将尝试对它做些什么,或者它将是您忽略的垃圾。

简单的答案是,当一个操作不可能(因为应用程序或者因为它违反业务逻辑)时。如果调用了一个方法,但不可能完成该方法编写时要做的事情,则抛出异常。一个很好的例子是,如果不能使用提供的参数创建实例,构造函数总是抛出argumentexception。另一个例子是InvalidOperationException,当一个操作由于类的另一个或多个成员的状态而无法执行时抛出该异常。

在您的情况下,如果调用Login(用户名,密码)这样的方法,如果用户名无效,抛出UserNameNotValidException或PasswordNotCorrectException(密码不正确)确实是正确的。用户不能使用提供的参数登录(即,这是不可能的,因为它将违反身份验证),因此抛出异常。尽管我可能从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;
}
}

我想说,基本上每一个原教旨主义都会导致地狱。

您当然不希望以异常驱动流结束,但是完全避免异常也是一个坏主意。你必须在两种方法之间找到平衡。我不会为每种异常情况创建异常类型。这是没有成效的。

我通常更喜欢创建两种基本类型的异常,它们在整个系统中使用:LogicalExceptionTechnicalException。如果需要,可以通过子类型进一步区分这些类型,但通常不是没有必要。

技术异常指的是真正意想不到的异常,比如数据库服务器宕机,到web服务的连接抛出IOException等等。

另一方面,逻辑异常用于将不太严重的错误情况传播到上层(通常是一些验证结果)。

请注意,即使是逻辑异常也不是为了定期使用来控制程序流,而是为了突出显示流何时应该真正结束的情况。在Java中使用时,这两种异常类型都是RuntimeException子类,错误处理是高度面向方面的。

因此,在登录示例中,创建类似AuthenticationException的东西并通过枚举值(如UsernameNotExistingPasswordMismatch等)来区分具体情况可能是明智的。这样就不会产生巨大的异常层次结构,并且可以将捕获块保持在可维护的级别。您还可以轻松地使用一些通用的异常处理机制,因为您已经对异常进行了分类,并且非常清楚要向用户传播什么以及如何传播。

我们的典型用法是,当用户输入无效时,在Web服务调用期间抛出LogicalException。异常被编组到SOAPFault详细信息,然后在客户机上再次被解组到异常,这将导致在某个web页面输入字段上显示验证错误,因为异常已正确映射到该字段。

这当然不是唯一的情况:您不需要点击web服务来抛出异常。你可以在任何特殊情况下自由地这样做(比如在你需要快速失败的情况下)——这都取决于你的判断。

我的小指南很大程度上受到了“代码完成”这本伟大的书的影响:

  • 使用异常来通知不应该被忽略的事情。
  • 如果错误可以在本地处理,就不要使用异常
  • 确保异常与例程的其余部分处于相同的抽象级别。
  • 应该为真正出色的保留异常。

我对异常的使用有哲学问题。基本上,您期待一个特定的场景发生,但不是明确地处理它,而是将问题推到“其他地方”处理。至于“其他地方”在哪里,谁也说不准。

在我看来,最基本的问题应该是,如果发生条件,调用者是否希望继续正常的程序流程。如果你不知道,要么有单独的doSomething和trySomething方法,前者返回一个错误,后者不返回,要么有一个接受参数的例程,以指示在失败时是否应该抛出异常)。考虑一个向远程系统发送命令并报告响应的类。某些命令(例如restart)会导致远程系统发送响应,但在一段时间内没有响应。因此,能够发送一个“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()例程抛出异常会更有帮助。

实际上,在上面的场景中,有一个参数来选择一些失败处理模式可能会有所帮助:从不抛出异常,仅为通信错误抛出异常,或者在命令没有返回“成功”指示的任何情况下抛出异常。

“PasswordNotCorrectException”不是一个使用异常的好例子。用户输入错误的密码是意料之中的,所以在我看来,这几乎不是个例外。您甚至可能从中恢复,显示一个漂亮的错误消息,因此这只是一个有效性检查。

未处理的异常将最终停止执行——这是好事。如果返回false、null或错误代码,则必须自己处理程序的状态。如果你忘记检查某个地方的条件,你的程序可能会用错误的数据继续运行,并且你可能很难弄清楚什么 happens和在哪里

当然,空的catch语句也可能导致同样的问题,但至少发现这些语句更容易,而且不需要理解逻辑。

所以根据经验:

在您不想要或无法从错误中恢复的地方使用它们。

当必需的技术或业务规则失败时,应该抛出异常。 例如,如果一个汽车实体与4个轮胎的数组相关联……如果一个或多个轮胎是空的…一个异常应该被触发“NotEnoughTiresException”,因为它可以在系统的不同级别被捕获,并通过日志记录具有重要意义。 此外,如果我们只是试图流控制null并防止汽车的实例化。 我们可能永远也找不到问题的根源,因为轮胎本来就不应该是空的。< / p >

抛出异常的经验法则非常简单。当你的代码进入UNRECOVERABLE INVALID状态时,你可以这样做。如果数据被泄露,或者您无法回溯到目前为止发生的处理,那么您必须终止它。你还能做什么呢?您的处理逻辑最终将在其他地方失败。如果你能以某种方式恢复,那么就这样做,不要抛出异常。

在你的特定情况下,如果你被迫做一些愚蠢的事情,比如接受提款,然后才检查用户/密码,你应该通过抛出一个异常来终止这个过程,通知发生了一些不好的事情,并防止进一步的损害。

以下是我的建议:

我不认为这总是一个抛出异常的好方法,因为它将花费更多的时间和内存来处理这样的异常。

在我心中,如果某些事情可以用“友好的,礼貌的”方式处理(这意味着如果我们可以“使用if......或类似的东西来谓词这样的错误),我们应该避免使用“异常”,而只是返回一个像“false”这样的标志,带有一个外部参数值告诉他/她详细的原因。

举个例子,我们可以这样创建一个类:

public class ValueReturnWithInfo<T>
{
public T Value{get;private set;}
public string errorMsg{get;private set;}
public ValueReturnWithInfo(T value,string errmsg)
{
Value = value;
errMsg = errmsg;
}
}

我们可以使用这种“多值返回”类来代替错误,这似乎是处理异常问题的一种更好、更礼貌的方式。

但是,请注意如果使用"if"......不能如此容易地描述某些错误(这取决于您的编程经验)(例如FileIO异常),则必须抛出异常.;

我想说的是,如果发生了意想不到的行为,应该抛出异常。

比如试图更新或删除一个不存在的实体。它应该在异常可以处理并且有意义的地方被捕获。如果要以另一种方式继续工作,请在Api级别上添加日志记录或返回特定的结果。

如果您期望某些事情是这样的,那么您应该构建代码来检查并确保它是正确的。