关于语言不可知论:什么时候抛出异常?

When to throw an exception?

对于我的应用程序没有预料到的每一个条件,我都有例外。UserNameNotValidExceptionPasswordNotCorrectException等。

然而,有人告诉我,我不应该为这些条件创建例外。在我的UML中,这些都是主流程的异常,那么为什么它不应该是异常呢?

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


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

示例1:假设我有一个函数,它检查一个任意类,如果该类继承了列表<>,则返回true。这个函数问了一个问题:"这个对象是列表的后代吗?"这个函数不应该抛出一个异常,因为它的操作中没有灰色区域——每一个类都继承或不从列表< >继承,所以答案总是"是"或"否"。

示例2:假设我有另一个函数来检查列表< >,如果长度大于50,则返回true,如果长度小于则返回false。此函数询问问题:"此列表中的项目是否超过50个?"但这个问题有一个假设——它假设它所给出的对象是一个列表。如果我给它一个空值,那么这个假设是错误的。在这种情况下,如果函数返回"真"或"假",那么它将破坏自己的规则。函数不能返回任何内容并声明它正确回答了问题。所以它不会返回-它抛出一个异常。

这与"加载问题"的逻辑谬误相当。每个函数都会问一个问题。如果给出的输入使该问题成为一个谬论,那么抛出一个异常。使用返回void的函数很难绘制此行,但底线是:如果违反了函数对其输入的假设,则应该抛出异常而不是正常返回。

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


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


我的小指南深受《代码完成》一书的影响:

  • 使用异常通知不应忽略的事情。
  • 如果错误可以在本地处理,则不要使用异常
  • 确保异常与您的其他例程处于相同的抽象级别。
  • 对于真正的例外,应该保留例外。

如果用户名无效或密码不正确,则不是例外。这些是您在正常操作流程中应该预料到的事情。异常是指那些不是正常程序操作的一部分,而且非常罕见的情况。

编辑:我不喜欢使用异常,因为您不能通过查看调用来判断方法是否引发异常。这就是为什么只有当你不能以一种体面的方式处理情况时才应该使用异常(想想"内存不足"或"计算机着火了")。


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


其他人则认为不应该使用异常,因为如果用户输入错误,那么错误的登录将出现在正常的流中。我不同意,也不明白为什么。将其与打开文件进行比较。如果文件不存在或者由于某种原因不可用,那么框架将抛出异常。使用上面的逻辑,这是微软的一个错误。他们应该返回一个错误代码。用于解析、webrequests等。

我不认为一个正常流程的错误登录部分,这是异常的。通常用户输入正确的密码,文件确实存在。例外情况是例外的,为这些情况使用例外是完全正确的。通过将返回值向上传播到堆栈的n个级别来使代码复杂化是浪费精力,并且会导致代码混乱。做最简单的事情。不要过早地使用错误代码进行优化,定义上的异常很少发生,除非抛出异常,否则异常不会花费任何代价。


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

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


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

你会看到很多建议,包括在这个问题的答案中,你应该只在"例外"的情况下抛出例外。这似乎表面上是合理的,但却是有缺陷的建议,因为它用另一个主观问题("什么是例外")代替了一个问题("我应该何时抛出例外")。相反,请遵循Habor萨特的建议(对于C++,可在多布斯博士文章中使用和何时使用异常,以及在他的书中使用Andrei Alexandrescu、C++编码标准):抛出异常,如果且仅当

  • 不满足前提条件(这通常会导致以下情况之一不可能)
  • 另一种办法是不能满足岗位条件。
  • 替代方案将无法保持不变。

为什么这样更好?它不是用几个前提条件、后置条件和不变量来代替这个问题吗?出于几个相关的原因,这样做更好。

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

我要说的是,在何时使用异常方面没有严格而快速的规则。但是,有充分的理由使用或不使用它们:

使用例外的原因:

  • 普通情况下的代码流更清晰
  • 可以作为一个对象返回复杂的错误信息(尽管也可以通过引用传递的错误"out"参数来实现)
  • 语言通常为在异常情况下管理整洁清理提供一些便利性(尝试/最终在Java中,在C++中使用,RAII在C++中使用)
  • 如果没有抛出异常,执行有时可能比检查返回代码更快。
  • 在爪哇中,必须检查或捕获检查异常(尽管这可能是反对的理由)。

不使用例外的原因:

  • 有时候,如果错误处理简单的话,就太过分了
  • 如果没有记录或声明异常,则调用代码可能不会捕获异常,这可能比调用代码刚刚忽略返回代码更糟(应用程序退出与静默失败-这可能更糟,取决于场景)。
  • 在C++中,使用异常的代码必须是异常安全的(即使您不抛出或捕获它们,而是间接调用抛掷函数)。
  • 在C++中,很难判断函数何时抛出,因此,如果使用异常安全性,则必须偏执于异常安全性。
  • 与检查返回标志相比,抛出和捕获异常的开销通常要大得多。

一般来说,我更倾向于使用Java中的异常,而不是C++或C语言中的异常,因为我认为,声明或不声明的异常基本上是函数的正式接口的一部分,因为更改异常保证可能会破坏调用代码。在Java IMO中使用它们的最大优点是,您知道调用方必须处理异常,这提高了正确行为的机会。

因此,在任何语言中,我总是从一个公共类中派生出代码层或API层中的所有异常,这样调用代码就可以保证捕获所有异常。此外,当编写API或库时,也会考虑抛出特定于实现的异常类(也就是说,从较低层封装异常,这样,调用方接收到的异常在接口的上下文中是可以理解的)。

注意,Java在通用和运行时异常之间进行区分,后者不需要声明。当您知道错误是程序中的错误的结果时,我只会使用运行时异常类。


异常类类似于"普通"类。当新类"是"不同类型的对象,具有不同的字段和不同的操作时,可以创建新类。

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


如果代码在一个循环中运行,可能会一次又一次地导致异常,那么抛出异常不是好事,因为对于大型N来说,它们非常慢,但是如果性能不是问题,抛出自定义异常没有什么错。只是确保你有一个基本例外,它们都继承,称为BaseExchange或类似的东西。BaseExchange继承系统.Exchange,但所有异常都继承BaseExchange。你甚至可以有一个异常类型树来对相似的类型进行分组,但是这可能是也可能不是多余的。

因此,简短的回答是,如果它不会造成显著的性能损失(除非您抛出了许多异常情况,否则不应该这样做),那么继续进行。


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

在您的特定情况下,如果您被迫做一些愚蠢的事情,比如接受取款,然后只检查用户/pasword,那么您应该通过抛出异常来终止该过程,以通知已经发生了坏的事情并防止进一步的损坏。


我同意日本的做法——当你对手术结果不确定时,抛开一个承诺。调用API、访问文件系统、数据库调用等,只要您正在越过编程语言的"边界"。

我想补充一下,请随意抛出一个标准异常。除非你要做一些"不同"的事情(忽略、电子邮件、日志、显示Twitter鲸鱼图片等等),否则不要为自定义的异常而烦恼。


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

因为通常不可能知道对您的用户有用的是什么,更好的方法是具有特定的异常,但确保它们从一个公共类继承(例如,STD::异常或其在C++中的派生)。这允许客户端在选择它们时捕获特定的异常,或者如果它们不关心,则会出现更一般的异常。


通常,您希望对应用程序中可能发生的任何"异常"事件抛出异常。

在您的示例中,这两个异常看起来都是通过密码/用户名验证调用它们的。在这种情况下,可以说,有人会错误地输入用户名/密码,这并不罕见。

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

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


异常用于异常行为、错误、失败等事件。函数行为、用户错误等应由程序逻辑来处理。由于错误的帐户或密码是登录例程中逻辑流的预期部分,因此它应该能够处理这些情况而不产生异常。


我想说,一般来说,每一个原教旨主义都会导致地狱。

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

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

技术异常表示真正意外的异常,如数据库服务器关闭、与Web服务的连接引发IOException等。

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

请注意,即使逻辑异常也不打算定期用于控制程序流,而是强调流真正结束的情况。当在Java中使用时,两种异常类型都是运行时异常子类,错误处理是面向方面的。

因此,在登录示例中,最好创建类似authenticationException的东西,并通过枚举值(如usernamenotexisting、passwordmismatch等)区分具体情况,这样您就不会最终拥有一个巨大的异常层次结构,并且可以将catch块保持在可维护级别。您还可以很容易地使用一些通用的异常处理机制,因为您已经对异常进行了分类,并且非常清楚应该向用户传播什么以及如何传播。

我们的典型用法是当用户的输入无效时,在Web服务调用期间抛出LogicalException。异常被封送到soapfault详细信息,然后在客户端上再次取消封送到异常,这导致在某个网页输入字段上显示验证错误,因为该异常具有到该字段的正确映射。

这当然不是唯一的情况:您不需要点击Web服务来抛出异常。在任何特殊情况下,你都可以这样做(比如你需要快速失败),这完全由你自己决定。


我对使用例外有哲学上的问题。基本上,你正在期待一个特定的场景发生,但不是明确地处理它,而是把问题推到其他地方去处理,而"别处"是任何人可以猜测的。


抛出异常会导致堆栈展开,这会对性能产生一些影响(公认的是,现代管理环境已经对此进行了改进)。在嵌套的情况下重复地抛出和捕获异常是一个坏主意。

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


避免引发异常的主要原因是,引发异常涉及大量开销。

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

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

以下是.NET中异常的一个不错的起点:http://msdn.microsoft.com/en-us/library/ms229030(vs.80).aspx


我发现了三种情况。

  • 错误或丢失的输入不应是例外。使用客户端JS和服务器端regex来检测、设置属性,并将消息转发回同一页。

  • 例外。这通常是您在代码中检测并抛出的一个异常。换句话说,这些是您期望的(文件不存在)。记录它,设置消息,然后转发回常规错误页。这个页面通常有一些关于发生了什么的信息。

  • 意外的异常。这些是你不知道的。记录详细信息并将其转发到常规错误页。

  • 希望这有帮助


    安全性与您的示例相混淆:您不应该告诉攻击者用户名存在,但密码错误。这是你不需要分享的额外信息。只需说"用户名或密码不正确"。


    简单的答案是,无论何时操作都不可能(因为应用程序或是违反业务逻辑)。如果调用方法并且不可能执行该方法所编写的操作,则抛出异常。一个很好的例子是,如果不能使用提供的参数创建实例,则构造函数总是抛出AgMuffExtExts。另一个例子是InvalidOperationException,它是在由于另一个或多个类成员的状态而无法执行操作时引发的。

    在您的情况下,如果调用了诸如login(用户名、密码)这样的方法,如果用户名无效,则抛出username NotvalidException或passwordNotCorrectException(如果密码不正确)确实是正确的。无法使用提供的参数登录用户(即不可能,因为它会违反身份验证),因此引发异常。尽管我可能会让你的两个异常继承自ArgumentException。

    尽管如此,如果您不希望抛出异常,因为登录失败可能非常常见,一种策略是创建一个返回表示不同失败的类型的方法。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    { // 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()模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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;
        }
    }

    在我看来,最基本的问题应该是,如果发生了某个条件,是否会期望调用者继续正常的程序流。如果您不知道,可以使用单独的dosomething和trymething方法,前者返回错误,而后者不返回错误,或者使用接受参数的例程来指示在异常失败时是否应引发异常)。考虑将命令发送到远程系统并报告响应的类。某些命令(例如重启)会导致远程系统发送响应,但在一定时间内没有响应。因此,可以发送一个"ping"命令,并查明远程系统是否在合理的时间内响应,而不必抛出异常(调用方可能希望前几次"ping"尝试失败,但最终会成功),这非常有用。另一方面,如果有一个命令序列,比如:

    1
    2
    3
    4
    5
    6
    7
      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()例程抛出异常会更有用。

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


    对于我来说,当所需的技术或业务规则失败时,应该抛出异常。例如,如果一个汽车实体与4个轮胎数组相关联…如果一个或多个轮胎无效…应该触发一个异常"notenoughtiresException",因为它可以在系统的不同级别捕获,并且通过日志记录具有重要意义。另外,如果我们只是试图控制零位流,防止汽车的启动。我们可能永远也找不到问题的根源,因为轮胎一开始就不应该是空的。


    最终的决定取决于使用异常处理来处理这样的应用程序级错误是否更有帮助,或者通过您自己的内部滚动机制(如返回状态代码)来处理。我不认为有一个硬性和快速的规则哪个更好,但我会考虑:

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

    对于这种情况,您可以使用一些通用的异常。例如,当方法的参数出现任何错误时(ArgumentNullException除外),就应该使用ArgumentException。一般来说,您不需要像lessthanzeroexception、notprimenumberexception等异常。请考虑您的方法的用户。她要专门处理的条件的数目等于您的方法需要抛出的异常类型的数目。这样,您就可以确定将有多详细的异常。

    顺便说一句,始终尝试为库的用户提供一些避免异常的方法。Tryparse是一个很好的例子,它的存在使您不必使用int.parse并捕获异常。在您的情况下,您可能需要提供一些方法来检查用户名是否有效或密码是否正确,这样您的用户(或您)就不必进行大量异常处理。这将有希望得到更可读的代码和更好的性能。


    有两类主要的异常:

    1)系统异常(如数据库连接丢失)或2)用户异常。(例如用户输入验证,"密码不正确")

    我发现创建自己的用户异常类很有帮助,当我想抛出一个用户错误时,我希望以不同的方式处理(即向用户显示资源错误),那么我在主错误处理程序中需要做的就是检查对象类型:

    1
    2
    3
    4
    5
    6
                If TypeName(ex) ="UserException" Then
                   Display(ex.message)
                Else
                   DisplayError("An unexpected error has occured, contact your help  desk")                  
                   LogError(ex)
                End If

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

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

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


  • "passwordNotCorrectException"不是使用异常的好例子。用户密码错误是意料之中的,所以这几乎不例外。您甚至可以从中恢复,显示一条很好的错误消息,所以这只是一个有效性检查。

    未处理的异常最终将停止执行-这很好。如果返回的是假、空或错误代码,则必须自己处理程序的状态。如果您忘记在某个地方检查条件,您的程序可能会使用错误的数据继续运行,并且您可能很难确定发生了什么以及在哪里发生了什么。

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

    因此,根据经验法则:

    无论您不想或只是无法从错误中恢复,都可以使用它们。


    当您想在返回值中表示两个错误状态或更多时,创建新的异常。


    异常与返回错误代码的参数应该是关于流控制而不是哲学的(错误是如何"异常的"):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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……或类似的东西来预测此类错误"),我们应该避免使用"异常",但只返回一个类似"假"的标志,外部参数值告诉他/她详细的原因。

    例如,我们可以这样做一个类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 exceptions)无法如此轻松地描述某些错误(这取决于您的编程经验),则必须抛出异常。