我为应用程序不期望出现的每个条件创建了异常。例如 UserNameNotValidException
,PasswordNotCorrectException
等等。
然而,有人告诉我不应该为这些情况创建异常。在我的UML中,它们确实是主流程之外的异常,那么为什么不应该是异常呢?
有关创建异常的指导或最佳实践方面的任何指导吗?
我为应用程序不期望出现的每个条件创建了异常。例如 UserNameNotValidException
,PasswordNotCorrectException
等等。
然而,有人告诉我不应该为这些情况创建异常。在我的UML中,它们确实是主流程之外的异常,那么为什么不应该是异常呢?
有关创建异常的指导或最佳实践方面的任何指导吗?
我的个人准则是:当当前代码块的基本假设被发现为假时,就会抛出异常。
例1:假设我有一个函数,它应该检查任意一个类,并返回true,如果该类从List<>继承。这个函数问的问题是,“这个对象是否是List<>的后代?”这个函数永远不应该抛出异常,因为它的操作没有灰色地带-每个类要么继承自List<>,要么不继承,所以答案总是“是”或“否”。
例2:假设我有另一个函数,它检查一个 List<> 并在长度超过50时返回true,在长度小于50时返回false。该函数问的问题是,“这个列表是否有超过50个项目?”但这个问题是有假设的——它假定所给的对象是一个列表。如果我将NULL交给它,那么这个假设就是错误的。在这种情况下,如果函数返回 true 或 false ,那么它违反了自己的规则。该函数不能返回 任何东西 并声称它正确回答了问题。因此它不返回值-而是抛出异常。
这类似于“多个问题”的谬论。每个函数都会提出一个问题。如果输入使该问题成为谬论,那么就抛出异常。对于返回void的函数,这条线更难画出,但底线是:如果函数对其输入的假设被违反,它应该抛出异常而不是正常返回。
另一方面,如果你发现你的函数经常抛出异常,那么你可能需要修改它们的假设。
因为它们是正常发生的事情。异常情况并不是控制流机制。用户经常输错密码,这不是一个异常情况。异常应该是真正罕见的事情,如UserHasDiedAtKeyboard
类型的情况。
我的小建议受到伟大的书籍《Code complete》的很大影响:
如果用户名无效或密码不正确,这不是异常情况。这些是您在正常操作流程中应该预期的事情。异常是不属于正常程序操作的罕见情况。
我不喜欢使用异常,因为你不能仅凭调用语句来判断方法是否会抛出异常。这就是为什么只有在无法以合适的方式处理情况时(比如“内存不足”或“计算机着火了”),才应该使用异常。
一个经验法则是在无法预测的情况下使用异常。例如,数据库连接、磁盘上缺少文件等。对于可以预测的情况,即用户尝试使用错误密码登录,您应该使用返回布尔值并且知道如何优雅地处理情况的函数。您不想仅因为某人输错密码而抛出异常而突然结束执行。
有些人认为异常不应该使用,因为如果用户输错了,那么错误的登录在正常流程中是可以预期的。我不同意这种观点,也不理解它的逻辑。拿打开文件来比较,如果文件不存在或由于某些原因无法打开,则框架将抛出异常。按照上述逻辑,这是 Microsoft 的错误。他们应该返回错误代码。同样适用于解析、Web 请求等等。
我不认为错误登录是正常流程的一部分,它是异常情况。通常情况下,用户会正确输入密码,文件确实存在。异常情况是特殊情况,对于这些情况使用异常完全没有问题。通过将返回值传播到堆栈的各个级别来使代码复杂化是一种浪费精力的做法,并且会导致混乱的代码。做最简单的可能运行的事情。不要过早地通过使用错误代码进行优化,因为异常情况很少发生,并且除非你抛出异常,否则不会产生任何成本。
throw
的决定是一个实现细节。它迫使我们记住,我们必须单独考虑设计和实现,并且我们在实现方法时的工作是生成满足设计约束的内容。catch
子句中。异常是一种比较昂贵的影响,例如如果用户提供了无效的密码,通常最好返回一个失败标志或其他指示它无效的数据。
这是因为异常的处理方式,真正的坏输入和独特的关键停止项应该是异常,但是登录信息不应该是异常。
我认为在何时使用异常没有硬性的规定。但是有使用或不使用它们的好理由:
使用异常的原因:
不使用异常的原因:
总的来说,我更倾向于在 Java 中使用异常,而不是在 C++ 或 C# 中,因为我认为异常(声明或不声明)基本上是函数的正式接口的一部分,因为更改您的异常保证可能会破坏调用代码。在我看来,在 Java 中使用它们的最大优点是,你知道你的调用者必须处理异常,这提高了正确行为的机会。
由于这个原因,在任何编程语言中,我总是从一个共同的类派生出所有异常层的代码或API,以便调用的代码始终可以保证捕获所有异常。此外,在编写API或库时,我认为抛出特定于实现的异常类是不好的(即,将来自较低层的异常包装起来,以便你的调用者在接口上下文中理解所接收到的异常)。int.MaxValue
次的循环,并在其中生成“除以零”的异常。IF/ELSE 版本中,我在执行除法操作之前检查被除数是否为零,完成时间为 6082 毫秒和 15407722 个时钟周期;而 TRY/CATCH 版本中,我生成异常并捕获异常,完成时间为 28174385 毫秒和 71371326155 个时钟周期:比 IF/ELSE 版本慢了整整 4632 倍。 - user1451111
IsCredentialsValid(用户名,密码)
的方法,如果用户名或密码无效,不应抛出异常,而应返回false。但如果身份验证失败,从数据库读取数据的方法可能合法地抛出此类异常。简而言之:如果一个方法无法完成它应该完成的任务,应该抛出异常。 - JacquesB