我为我的应用程序不期望的每个条件创建了异常. UserNameNotValidException
,PasswordNotCorrectException
等等.
但是我被告知我不应该为这些条件创建例外.在我的UML中,那些是主流的例外,为什么它不应该是例外?
创建例外的任何指导或最佳实践?
我的个人指南是:当发现当前代码块的基本假设为假时,抛出异常.
示例1:假设我有一个应该检查任意类的函数,如果该类继承自List <>,则返回true.这个函数问一个问题,"这个对象是List的后代吗?" 这个函数永远不会抛出异常,因为它的操作中没有灰色区域 - 每个类都是从List <>继承或不继承,所以答案总是"是"或"否".
示例2:假设我有另一个检查List <>的函数,如果长度大于50则返回true,如果长度小则返回false.此功能询问"此列表是否包含超过50个项目?" 但是这个问题做了一个假设 - 它假设给出的对象是一个列表.如果我把它交给NULL,那么这个假设就是假的.在这种情况下,如果函数返回无论是真还是假的,那么它是打破自己的规则.该功能无法返回任何内容,并声称它正确回答了问题.所以它不会返回 - 它会引发异常.
这与"加载问题"逻辑谬误相当.每个功能都会提出一个问题.如果给出的输入使得该问题成为谬误,则抛出异常.这行很难用返回void的函数绘制,但底线是:如果违反了函数关于其输入的假设,它应抛出异常而不是正常返回.
这个等式的另一面是:如果你发现你的函数经常抛出异常,那么你可能需要改进它们的假设.
因为它们是正常发生的事情.例外不是控制流机制.用户经常会弄错密码,这不是特例.UserHasDiedAtKeyboard
类型情况下,例外情况应该是非常罕见的.
我的小指南深受"伟大的代码"这本伟大的书的影响:
使用例外来通知不应忽略的事情.
如果可以在本地处理错误,请不要使用异常
确保异常与其他例程处于相同的抽象级别.
应该保留例外以用于真正特殊的事情.
如果用户名无效或密码不正确,则不例外.这些是您在正常操作流程中应该预期的事情.例外情况不是正常程序操作的一部分,而且非常罕见.
编辑:我不喜欢使用异常,因为您无法判断方法是否仅通过查看调用就抛出异常.这就是为什么只有在你不能以体面的方式处理这种情况时才应该使用例外情况(想想"内存不足"或"计算机着火了").
一个经验法则是在您通常无法预测的情况下使用例外.例如数据库连接,磁盘上缺少文件等.对于您可以预测的场景,即用户尝试使用错误密码登录,您应该使用返回布尔值的函数并知道如何正常处理这种情况.您不希望因为有人输错密码而突然终止执行.
其他人建议不应该使用异常,因为如果用户输入错误,则在正常流程中会出现错误登录.我不同意,我没有得到推理.将它与打开文件进行比较..如果文件不存在或由于某种原因不可用,则框架将抛出异常.使用上面的逻辑,这是微软的一个错误.他们应该返回错误代码.解析,webrequests等等也是如此.
我不认为正常流程的登录部分是错误的,这是特殊的.通常,用户键入正确的密码,文件确实存在.特殊情况非常特殊,使用例外情况完全没问题.通过在堆栈中向上传播返回值来使代码复杂化是浪费精力并导致代码混乱.做最简单的事可能有用.不要通过使用错误代码过早地优化,根据定义很少发生异常的事情,除非你抛出异常,否则异常不会花费任何成本.
例外是一种有点代价高昂的效果,例如,如果您有一个提供无效密码的用户,通常最好传回失败标志或其他指示它无效的指示.
这是由于处理异常的方式,真正的错误输入和唯一的关键停止项应该是例外,但不是失败的登录信息.
我认为只有在你无法摆脱当前状态时才应该抛出异常.例如,如果您正在分配内存并且没有任何要分配的内存.在您提到的情况下,您可以清楚地从这些状态恢复,并可以相应地将错误代码返回给您的调用者.
你会看到很多建议,包括在这个问题的答案中,你应该只在"特殊"情况下抛出异常.这似乎是表面上合理的,但是有缺陷的建议,因为它用另一个主观问题("什么是例外")取代了一个问题("何时应该抛出异常").相反,请遵循Herb Sutter的建议(对于C++,可在Dobbs博士的文章中找到何时以及如何使用例外,以及在他的书中使用Andrei Alexandrescu,C++编码标准):如果且仅当
不满足前提条件(通常使下列其中一项不可能)或
替代方案将无法满足后置条件或
替代方案将无法保持不变.
为什么这样更好?它是否用几个关于先决条件,后置条件和不变量的问题取代了这个问题?由于几个相关的原因,这更好.
前提条件,后置条件和不变量是我们程序(其内部API)的设计特征,而决定throw
是实现细节.它迫使我们记住,我们必须分别考虑设计及其实现,而我们在实现方法时的工作是产生满足设计约束的东西.
它迫使我们根据前提条件,后置条件和不变量进行思考,这是我们方法的调用者应该做出的唯一假设,并且是精确表达的,能够在我们程序的组件之间实现松散耦合.
然后松散耦合允许我们在必要时重构实现.
后置条件和不变量是可测试的; 它导致代码可以很容易地进行单元测试,因为后置条件是我们的单元测试代码可以检查(断言)的谓词.
在后置条件方面进行思考自然会产生一种作为后置条件成功的设计,这是使用例外的自然风格.程序的正常("快乐")执行路径是线性布局的,所有错误处理代码都移到了catch
子句中.
我想说什么时候使用异常没有硬性规定.但是,有充分的理由使用或不使用它们:
使用例外的原因:
常见情况的代码流更清晰
可以将复杂的错误信息作为对象返回(虽然这也可以通过引用传递的错误"out"参数来实现)
语言通常提供一些工具来管理异常事件中的整洁清理(在Java中尝试/最终,在C#中使用,在C++中使用RAII)
如果没有抛出异常,执行有时可能比检查返回代码更快
在Java中,必须声明或捕获已检查的异常(尽管这可能是一个原因)
不使用例外的原因:
如果错误处理很简单,有时候会有点过分
如果未记录或声明异常,则可能通过调用代码来解决它们,这可能比调用代码忽略返回代码更糟糕(应用程序退出与静默失败 - 更糟糕的可能取决于方案)
在C++中,使用异常的代码必须是异常安全的(即使你不抛出或捕获它们,但间接调用throw函数)
在C++中,很难判断函数何时可能抛出,因此如果使用它们,您必须对异常安全感到偏执
与检查返回标志相比,抛出和捕获异常通常要贵得多
一般来说,我更倾向于在Java中使用异常而不是在C++或C#中使用异常,因为我认为异常(声明与否)从根本上是函数形式接口的一部分,因为更改异常保证可能打破调用代码.在Java IMO中使用它们的最大优点是,您知道您的调用者必须处理异常,这样可以提高正确行为的可能性.
因此,在任何语言中,我总是在公共类的代码或API层中派生所有异常,因此调用代码始终可以保证捕获所有异常.另外,我认为在编写API或库时抛出特定于实现的异常类是不好的(即从较低层包装异常,以便调用者接收的异常在您的接口上下文中是可理解的).
请注意,Java区分了一般异常和运行时异常,因为后者不需要声明.当你知道错误是程序中的错误的结果时,我只会使用运行时异常类.
异常类就像"普通"类.当它"是"不同类型的对象,具有不同的字段和不同的操作时,您创建一个新类.
根据经验,您应该尝试在异常数量和异常粒度之间取得平衡.如果您的方法抛出超过4-5个不同的异常,您可以将其中一些异常合并到更多"常规"异常中(例如,在您的情况下为"AuthenticationFailedException"),并使用异常消息来详细说明出错的地方.除非您的代码以不同方式处理每个代码,否则不需要创建许多异常类.如果确实如此,您可能应该只返回出现错误的枚举.这种方式有点干净.
如果它的代码在一个循环中运行,可能会一遍又一遍地引起异常,那么抛出异常并不是一件好事,因为它们对于大N来说非常慢.但是如果性能不是这样,抛出自定义异常没有错一个问题.只要确保你有一个他们都继承的基本异常,称为BaseException或类似的东西.BaseException继承System.Exception,但所有异常都继承BaseException.您甚至可以使用异常类型树来对相似类型进行分组,但这可能是也可能不是过度杀伤.
所以,简短的回答是,如果它不会导致显着的性能损失(除非你抛出很多异常,否则不应该这样做),然后继续.