我为我的应用程序不期望的每个条件创建了异常。 UserNameNotValidException
等PasswordNotCorrectException
。
但是我被告知我不应该为这些条件创建例外。在我的 UML 中,那些是主要流程的异常,那么为什么它不应该是异常呢?
创建例外的任何指导或最佳实践?
我为我的应用程序不期望的每个条件创建了异常。 UserNameNotValidException
等PasswordNotCorrectException
。
但是我被告知我不应该为这些条件创建例外。在我的 UML 中,那些是主要流程的异常,那么为什么它不应该是异常呢?
创建例外的任何指导或最佳实践?
我个人的指导方针是:当发现当前代码块的基本假设为错误时,将引发异常。
示例 1:假设我有一个函数应该检查任意类并在该类继承自 List<> 时返回 true。这个函数提出了一个问题,“这个对象是 List 的后代吗?” 这个函数不应该抛出异常,因为在它的操作中没有灰色区域——每个单独的类要么继承自 List<>,要么不继承,所以答案总是“是”或“否”。
示例 2:假设我有另一个函数检查 List<> 并在其长度大于 50 时返回 true,如果长度小于则返回 false。这个函数提出了一个问题,“这个列表有超过 50 个项目吗?” 但是这个问题做了一个假设——它假设给出的对象是一个列表。如果我给它一个NULL,那么这个假设是错误的。在这种情况下,如果函数返回true或false ,那么它就违反了自己的规则。该函数无法返回任何内容并声称它正确回答了问题。所以它不会返回 - 它会引发异常。
这与“加载问题”的逻辑谬误相当。每个函数都会提出一个问题。如果给出的输入使该问题成为谬误,则抛出异常。使用返回 void 的函数很难画出这条线,但底线是:如果违反了函数对其输入的假设,它应该抛出异常而不是正常返回。
这个等式的另一面是:如果你发现你的函数经常抛出异常,那么你可能需要改进它们的假设。
因为它们是会正常发生的事情。异常不是控制流机制。用户经常会弄错密码,这不是例外情况。异常应该是一个真正罕见的事情,UserHasDiedAtKeyboard
类型的情况。
我的小指南深受《代码完整》这本书的影响:
如果用户名无效或密码不正确,也不例外。这些是您在正常操作流程中应该期待的事情。异常是不属于正常程序操作的一部分并且相当罕见的事情。
编辑:我不喜欢使用异常,因为您无法仅通过查看调用来判断方法是否引发异常。这就是为什么只有在你不能以体面的方式处理这种情况时才应该使用异常(想想“内存不足”或“计算机着火”)。
一条经验法则是在您通常无法预测的情况下使用例外。示例是数据库连接、磁盘上的文件丢失等。对于您可以预测的情况,即用户尝试使用错误密码登录,您应该使用返回布尔值并知道如何优雅地处理这种情况的函数。您不希望仅仅因为有人输入错误的密码就抛出异常来突然结束执行。
其他人建议不应使用异常,因为如果用户输入错误,在正常流程中会出现错误登录。我不同意,我不明白理由。将其与打开文件进行比较。如果文件不存在或由于某种原因不可用,则框架将引发异常。使用上面的逻辑是微软的一个错误。他们应该返回一个错误代码。解析、webrequests 等也是如此。
我不认为正常流程中的不良登录部分是例外的。通常用户输入正确的密码,并且文件确实存在。例外情况是例外的,对这些情况使用例外是完全可以的。通过向上 n 层传播返回值来使代码复杂化是一种能源浪费,并且会导致代码混乱。做可能可行的最简单的事情。不要过早地使用错误代码进行优化,根据定义,异常的东西很少发生,除非你抛出异常,否则它们不会花费任何代价。
我认为只有在无法摆脱当前状态时才应该抛出异常。例如,如果您正在分配内存并且没有要分配的内存。在您提到的情况下,您可以清楚地从这些状态中恢复,并可以相应地将错误代码返回给您的调用者。
你会看到很多建议,包括在这个问题的答案中,你应该只在“异常”情况下抛出异常。这似乎表面上合理,但却是有缺陷的建议,因为它用另一个主观问题(“什么是例外”)代替了一个问题(“我什么时候应该抛出异常”)。相反,请遵循 Herb Sutter 的建议(对于 C++,可在Dobbs 博士的文章When and How to Use Exceptions以及他与 Andrei Alexandrescu 合着的书C++ Coding Standards中找到):当且仅当
为什么这样更好?它不是用几个关于前置条件、后置条件和不变量的问题来代替这个问题吗?由于几个相关的原因,这更好。
throw
是实现细节。它迫使我们牢记,我们必须分别考虑设计及其实现,而我们在实现方法时的工作是产生满足设计约束的东西。catch
子句中。异常会产生一些代价高昂的影响,例如,如果您有一个用户提供了无效密码,通常最好将失败标志或其他一些指示它无效的指示传回。
这是由于处理异常的方式,真正的错误输入和唯一的关键停止项应该是异常,而不是失败的登录信息。
我想说对于何时使用例外没有硬性规定。但是,使用或不使用它们有充分的理由:
使用异常的原因:
不使用异常的原因:
一般来说,我更倾向于在 Java 中使用异常而不是在 C++ 或 C# 中,因为我认为异常,无论是否声明,基本上都是函数的正式接口的一部分,因为更改你的异常保证可能中断调用代码。在 Java IMO 中使用它们的最大优势是您知道调用者必须处理异常,这提高了正确行为的机会。
正因为如此,在任何语言中,我总是会从一个公共类中派生出一层代码或 API 中的所有异常,以便调用代码始终保证捕获所有异常。此外,我认为在编写 API 或库时抛出特定于实现的异常类是不好的(即包装来自较低层的异常,以便调用者收到的异常在您的接口上下文中是可以理解的)。
请注意,Java 对一般异常和运行时异常进行了区分,因为后者不需要声明。当您知道错误是程序中的错误导致时,我只会使用运行时异常类。
如果它是在循环中运行的代码可能会一遍又一遍地导致异常,那么抛出异常并不是一件好事,因为它们对于大 N 来说非常慢。但是如果性能不是那么抛出自定义异常并没有什么问题一个问题。只要确保您有一个它们都继承的基本异常,称为 BaseException 或类似的东西。BaseException 继承 System.Exception,但您的所有异常都继承 BaseException。您甚至可以拥有一个异常类型树来对相似类型进行分组,但这可能会也可能不会过大。
所以,简短的回答是,如果它不会导致显着的性能损失(除非你抛出很多异常,否则它不应该),然后继续。
异常类就像“正常”类。当它“是”一种不同类型的对象,具有不同的字段和不同的操作时,您就创建了一个新类。
根据经验,您应该尝试在异常数量和异常粒度之间取得平衡。如果您的方法抛出超过 4-5 个不同的异常,您可能可以将其中一些合并到更“一般”的异常中(例如,在您的情况下为“AuthenticationFailedException”),并使用异常消息来详细说明出了什么问题。除非您的代码以不同方式处理它们中的每一个,否则您不需要创建许多异常类。如果确实如此,您是否应该只返回一个带有发生错误的枚举。这样会干净一些。
抛出异常的经验法则非常简单。当您的代码进入不可恢复的无效状态时,您会这样做。如果数据被泄露,或者您无法回退到此时发生的处理,那么您必须终止它。事实上,你还能做什么?您的处理逻辑最终将在其他地方失败。如果你能以某种方式恢复,那么就这样做并且不要抛出异常。
在您的特定情况下,如果您被迫做一些愚蠢的事情,例如接受提款,然后才检查用户/密码,您应该通过抛出异常来终止该过程,以通知发生了不好的事情并防止进一步的损害。
我同意 japollock 的观点——当你不确定手术的结果时,请接受。对 API 的调用、访问文件系统、数据库调用等。任何时候你都在超越你的编程语言的“边界”。
我想补充一点,请随意抛出一个标准异常。除非您打算做一些“不同”的事情(忽略、发送电子邮件、登录、显示 twitter 鲸鱼图片等),否则不要为自定义异常而烦恼。
通常,您希望为应用程序中可能发生的任何“异常”事件抛出异常
在您的示例中,这两个异常看起来都像是您通过密码/用户名验证调用它们。在这种情况下,可以说有人会输错用户名/密码并不是什么特别的事情。
它们是 UML 主要流程的“例外”,但在处理中更多的是“分支”。
如果您试图访问您的 passwd 文件或数据库但无法访问,那将是一个例外情况,需要引发异常。
首先,如果您的 API 的用户对特定的、细粒度的故障不感兴趣,那么为他们设置特定的异常就没有任何价值。
由于通常不可能知道什么对您的用户有用,因此更好的方法是拥有特定的异常,但要确保它们继承自一个公共类(例如,std::exception 或其在 C++ 中的派生类)。这允许您的客户在他们选择时捕获特定的异常,或者如果他们不关心,则可以捕获更一般的异常。
异常适用于异常行为、错误、故障等事件。功能行为、用户错误等应该由程序逻辑来处理。由于错误的帐户或密码是登录例程中逻辑流程的预期部分,因此它应该能够毫无例外地处理这些情况。
简单的答案是,只要操作是不可能的(因为任何一个应用程序或因为它会违反业务逻辑)。如果调用了一个方法并且无法执行编写该方法的目的,则抛出异常。一个很好的例子是,如果无法使用提供的参数创建实例,构造函数总是抛出 ArgumentExceptions。另一个示例是 InvalidOperationException,当由于类的另一个或多个成员的状态而无法执行操作时抛出该异常。
在您的情况下,如果调用像 Login(username, password) 这样的方法,如果用户名无效,则抛出 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))
{ }
}
大多数开发人员都被教导要避免异常,因为抛出异常会造成开销。有资源意识很好,但通常不会以牺牲您的应用程序设计为代价。这可能是您被告知不要抛出两个异常的原因。是否使用异常通常归结为异常发生的频率。如果这是一个相当普遍或相当可预期的结果,那么大多数开发人员将避免异常,而是创建另一种方法来指示失败,因为假定会消耗资源。
这是一个使用 Try() 模式避免在刚刚描述的场景中使用异常的示例:
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 服务来抛出异常。您可以在任何特殊情况下自由地这样做(例如在您需要快速失败的情况下) - 这完全由您自行决定。
对我来说,当所需的技术或业务规则失败时,应该抛出异常。例如,如果汽车实体与 4 个轮胎的数组相关联……如果一个或多个轮胎为空……应该触发一个异常 "NotEnoughTiresException" ,因为它可以在系统的不同级别被捕获并具有显着通过日志记录的意思。此外,如果我们只是尝试对 null 进行流控制并防止 car 的实例化。我们可能永远找不到问题的根源,因为轮胎本来就不应该是空的。
避免抛出异常的主要原因是抛出异常涉及大量开销。
下面的文章指出的一件事是异常是针对异常条件和错误。
错误的用户名不一定是程序错误,而是用户错误...
这是 .NET 中异常的一个不错的起点:http: //msdn.microsoft.com/en-us/library/ms229030 (VS.80).aspx
抛出异常会导致堆栈展开,这会对性能产生一些影响(承认,现代托管环境对此进行了改进)。在嵌套情况下仍然反复抛出和捕获异常将是一个坏主意。
可能比这更重要的是,例外是针对特殊情况的。它们不应该用于普通的控制流,因为这会损害代码的可读性。
我发现了三种情况。
输入错误或丢失不应该是例外。使用客户端 js 和服务器端正则表达式来检测、设置属性并转发回带有消息的同一页面。
应用异常。这通常是您在代码中检测并抛出的异常。换句话说,这些是您所期望的(文件不存在)。记录它,设置消息,然后转发回一般错误页面。这个页面通常有一些关于发生了什么的信息。
意外的异常。这些是你不知道的。记录详细信息并将它们转发到一般错误页面。
希望这可以帮助
安全性与您的示例混为一谈:您不应该告诉攻击者用户名存在,但密码错误。这是您不需要分享的额外信息。只需说“用户名或密码不正确”即可。
我在使用例外方面存在哲学问题。基本上,您期望发生特定情况,但不是明确地处理它,而是将问题推到“其他地方”处理。任何人都可以猜测“其他地方”在哪里。
在我看来,基本问题应该是如果发生某种情况,是否会期望调用者希望继续正常的程序流程。如果你不知道,要么有单独的 doSomething 和 trySomething 方法,前者返回错误而后者不返回,或者有一个例程接受一个参数来指示如果失败是否应该抛出异常)。考虑一个向远程系统发送命令并报告响应的类。某些命令(例如重新启动)会导致远程系统发送响应,但在一定时间内没有响应。因此,能够发送“ping”命令并找出远程系统是否在合理的时间长度内响应而不必抛出异常是很有用的 t(调用者可能会期望前几次“ping”尝试会失败,但最终会成功)。另一方面,如果有一系列命令,例如:
exchange_command("打开临时文件"); exchange_command("写临时文件数据 {whatever}"); exchange_command("写临时文件数据 {whatever}"); exchange_command("写临时文件数据 {whatever}"); exchange_command("写临时文件数据 {whatever}"); exchange_command("关闭临时文件"); exchange_command("将临时文件复制到真实文件");
人们会希望任何操作失败以中止整个序列。虽然可以检查每个操作以确保其成功,但如果命令失败,让 exchange_command() 例程抛出异常会更有帮助。
实际上,在上述场景中,有一个参数来选择多种故障处理模式可能会有所帮助:从不抛出异常,仅针对通信错误抛出异常,或在命令未返回“成功”的任何情况下抛出异常"指示。
“PasswordNotCorrectException”不是使用异常的好例子。用户密码错误是意料之中的,所以恕我直言,这几乎不是例外。你甚至可能从中恢复,显示一个很好的错误消息,所以它只是一个有效性检查。
未处理的异常最终会停止执行——这很好。如果您返回 false、null 或错误代码,您将不得不自己处理程序的状态。如果您忘记检查某处的条件,您的程序可能会继续运行错误数据,并且您可能很难弄清楚发生了什么以及发生在哪里。
当然,空的 catch 语句可能会导致同样的问题,但至少发现这些更容易,并且不需要您理解逻辑。
因此,根据经验:
在您不想要或根本无法从错误中恢复的任何地方使用它们。
您可以针对该条件使用一些通用异常。例如,当方法的参数出现任何问题时使用 ArgumentException(ArgumentNullException 除外)。通常你不需要像 LessThanZeroException、NotPrimeNumberException 等异常。想想你的方法的用户。她要专门处理的条件的数量等于您的方法需要抛出的异常类型的数量。这样,您可以确定您将拥有的异常详细程度。
顺便说一句,请始终尝试为您的库的用户提供一些避免异常的方法。TryParse 是一个很好的例子,它的存在使您不必使用 int.Parse 并捕获异常。在您的情况下,您可能希望提供一些方法来检查用户名是否有效或密码是否正确,这样您的用户(或您)就不必进行大量异常处理。这有望带来更易读的代码和更好的性能。
最终决定归结为使用异常处理或通过您自己的家庭滚动机制(如返回状态代码)来处理此类应用程序级错误是否更有帮助。我认为对于哪个更好没有硬性规定,但我会考虑:
在决定例外是否合适时需要考虑一些有用的事情:
在异常候选发生后您希望运行什么级别的代码 - 即调用堆栈应该展开多少层。您通常希望在尽可能接近异常发生的地方处理异常。对于用户名/密码验证,您通常会在同一代码块中处理失败,而不是让异常冒泡。所以例外可能是不合适的。(OTOH,在 3 次登录尝试失败后,控制流可能会转移到其他地方,此处可能会出现异常。)
您是否希望在错误日志中看到此事件?并非每个异常都写入错误日志,但询问错误日志中的此条目是否有用是很有用的 - 即,您会尝试对此做些什么,或者是否会成为您忽略的垃圾。
主要有两类异常:
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
异常与返回错误代码参数应该是关于流控制而不是哲学(错误是多么“异常”):
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
}
}
以下是我的建议:
我不认为抛出异常总是一个好方法,因为处理此类异常需要更多时间和内存。
在我看来,如果可以用“亲切、礼貌”的方式处理某些事情(这意味着如果我们可以“通过使用 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 异常),则必须抛出异常。