什么时候抛出异常?

时间:2008-09-16 21:09:02

标签: exception language-agnostic

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

但有人告诉我,我不应该为这些条件创建例外。在我的UML中,那些是主流的例外,为什么它不应该是一个例外?

创建例外的任何指导或最佳做法?

33 个答案:

答案 0 :(得分:576)

我的个人指南是:当发现当前代码块的基本假设是错误时,抛出异常。

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

示例2:假设我有另一个功能来检查List<>如果长度大于50,则返回true;如果长度小于,则返回false。此功能询问“此列表是否包含超过50个项目?”但是这个问题做了一个假设 - 它假设给出的对象是一个列表。如果我把它交给NULL,那么这个假设就是假的。在这种情况下,如果函数返回 true false,那么它就会违反自己的规则。该函数无法返回任何并声称它正确回答了问题。所以它不会返回 - 它会引发异常。

这与"loaded question"逻辑谬误相当。每个功能都会提出一个问题。如果给出的输入使得该问题成为谬误,则抛出异常。这行很难用返回void的函数绘制,但底线是:如果违反了函数关于其输入的假设,它应抛出异常而不是正常返回。

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

答案 1 :(得分:279)

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

答案 2 :(得分:60)

我的小指引深受“伟大的代码”这本伟大的书的影响:

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

答案 3 :(得分:34)

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

编辑:我不喜欢使用异常,因为你无法判断一个方法是否只是通过查看调用就抛出异常。这就是为什么只有在你不能以合适的方式处理这种情况时才应该使用例外情况(想想“内存不足”或“计算机着火了”)。

答案 4 :(得分:25)

一个经验法则是在您通常无法预测的情况下使用例外。例如数据库连接,磁盘上缺少文件等。对于您可以预测的场景,即用户尝试使用错误密码登录,您应该使用返回布尔值的函数并知道如何正常处理这种情况。你不想因为有人输错密码而突然终止执行。

答案 5 :(得分:23)

其他人建议不应使用异常,因为如果用户输入错误,则在正常流程中会出现错误登录。我不同意,我没有得到推理。将它与打开文件进行比较..如果文件不存在或由于某种原因不可用,则框架将抛出​​异常。使用上面的逻辑,这是微软的一个错误。他们应该返回错误代码。解析,webrequests等等也是如此。

我不认为正常流程的登录部分是错误的,这是例外。通常,用户键入正确的密码,文件确实存在。特殊情况非常特殊,使用例外情况完全没问题。通过在堆栈中向上传播返回值来使代码复杂化是浪费精力并导致代码混乱。做最简单的事可能有用。不要过早地使用错误代码进行优化,根据定义很少发生异常事件,除非你抛出异常,否则异常不需要花费任何费用。

答案 6 :(得分:16)

例外是一种有些代价高昂的效果,例如,如果您的用户提供的密码无效,通常最好还是传回失败标志,或者其他一些指示它无效。

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

答案 7 :(得分:14)

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


你会看到很多建议,包括在这个问题的答案中,你应该只在“例外”情况下抛出异常。这似乎是表面上合理的,但是有缺陷的建议,因为它用另一个主观问题(“什么是例外”)取代了一个问题(“何时应该抛出异常”)。相反,遵循Herb Sutter的建议(适用于C ++,可在Dr Dobbs article When and How to Use Exceptions中找到,也可以在他的书中找到Andrei Alexandrescu, C ++编码标准):当且仅当

  • 不满足前提条件(通常会产生下列情况之一) 不可能)或
  • 替代方案将无法满足后置条件或
  • 替代方案将无法保持不变。

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

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

答案 8 :(得分:10)

我想说什么时候使用例外没有硬性规定。但是,使用或不使用它们有充分的理由:

使用例外的原因:

  • 常见案例的代码流程更清晰
  • 可以将复杂的错误信息作为对象返回(虽然这也可以通过引用传递的错误“out”参数来实现)
  • 语言通常提供一些工具来管理异常事件中的整洁清理(在Java中尝试/最终,在C#中使用,在C ++中使用RAII)
  • 如果没有抛出异常,执行有时比检查返回代码更快
  • 在Java中,必须声明或捕获已检查的异常(尽管这可能是一个原因)

不使用例外的原因:

  • 如果错误处理很简单,有时会出现问题
  • 如果未记录或声明异常,则可能通过调用代码来解决它们,这可能比调用代码忽略返回代码更糟糕(应用程序退出与静默失败 - 更糟糕的可能取决于方案)
  • 在C ++中,使用异常的代码必须是异常安全的(即使你不抛出或捕获它们,但间接调用throw函数)
  • 在C ++中,很难判断某个函数何时可能抛出,因此如果你使用它们,你必须对异常安全感到偏执
  • 与检查返回标志相比,投掷和捕获异常通常要贵得多。

一般来说,我更倾向于在Java中使用异常而不是在C ++或C#中使用异常,因为我认为异常(声明与否)从根本上是函数的正式接口的一部分,因为更改你的异常保证可能会破坏调用代码。在Java IMO中使用它们的最大优点是,您知道您的调用者必须处理异常,这样可以提高正确行为的可能性。

因此,在任何语言中,我总是在公共类的代码或API层中派生所有异常,因此调用代码始终可以保证捕获所有异常。另外,我认为在编写API或库时抛出特定于实现的异常类是不好的(即从较低层包装异常,以便调用者接收的异常在您的接口上下文中是可理解的。)

请注意,Java区分了一般异常和运行时异常,因为后者不需要声明。当您知道错误是程序中的错误的结果时,我只会使用运行时异常类。

答案 9 :(得分:5)

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

根据经验,您应该尝试在异常数量和异常粒度之间取得平衡。如果您的方法抛出超过4-5个不同的异常,您可以将其中一些异常合并到更多“常规”异常中(例如在您的情况下为“AuthenticationFailedException”),并使用异常消息来详细说明出错的地方。除非您的代码以不同方式处理每个代码,否则不需要创建许多异常类。如果确实如此,您可能应该只返回出现错误的枚举。这种方式有点干净。

答案 10 :(得分:5)

如果它的代码在一个循环中运行,可能会一遍又一遍地引起异常,那么抛出异常并不是一件好事,因为它们对于大N来说非常慢。但是如果抛出自定义异常则没有任何问题。表现不是问题。只要确保你有一个他们都继承的基本异常,称为BaseException或类似的东西。 BaseException继承System.Exception,但所有异常都继承BaseException。您甚至可以使用异常树类型树来对相似类型进行分组,但这可能会或可能不会过度。

因此,简短的回答是,如果它不会导致显着的性能损失(除非你抛出很多异常,否则不应该这样做),然后继续。

答案 11 :(得分:3)

抛出异常的经验法则非常简单。当您的代码进入UNRECOVERABLE INVALID状态时,您会这样做。如果数据受到损害,或者您无法收回到该点发生的处理,那么您必须终止它。你还能做什么呢?你的处理逻辑最终会在别处失败。如果你能以某种方式恢复,那么就这样做,不要抛出异常。

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

答案 12 :(得分:3)

我同意japollock在那里的方式 - 当你不确定手术的结果时抛出接受。调用API,访问文件系统,数据库调用等。任何时候你都要超越编程语言的“边界”。

我想补充一下,随意抛出标准异常。除非你要做一些“不同”的事情(忽略,发电子邮件,登录,显示推特鲸鱼图片等等),否则不要打扰自定义例外。

答案 13 :(得分:2)

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

答案 14 :(得分:2)

我在使用例外方面存在哲学问题。基本上,您期望发生特定情况,但不是明确地处理它,而是将问题推迟到“其他地方”处理。那些“别处”的地方可以是任何人的猜测。

答案 15 :(得分:2)

我会说通常每个原教旨主义都会导致地狱。

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

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

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

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

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

因此,在登录示例中,创建类似AuthenticationException的东西并通过枚举值区分具体情况可能是明智的,例如 UsernameNotExisting PasswordMismatch 等等。那么你就不会最终拥有一个巨大的异常层次结构,并且可以将catch块保持在可维护的级别。您还可以轻松地使用一些通用的异常处理机制,因为您有分类的异常,并且非常清楚传播给用户的内容以及如何传播。

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

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

答案 16 :(得分:2)

首先,如果您的API用户对特定的细粒度故障不感兴趣,那么对它们有特定的例外情况则没有任何价值。

由于通常不可能知道对用户有用的内容,更好的方法是使用特定的异常,但确保它们从公共类继承(例如,std :: exception或其在C ++中的派生类)。这允许您的客户在他们选择时捕获特定的异常,或者如果他们不关心则捕获更一般的异常。

答案 17 :(得分:2)

一般情况下,您希望为应用程序中可能发生的任何“异常”事件抛出异常

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

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

如果您试图访问您的passwd文件或数据库而不能,那将是一个例外情况,并且可以保证抛出异常。

答案 18 :(得分:1)

我有三种类型的条件。

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

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

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

  4. 希望这有帮助

答案 19 :(得分:1)

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

答案 20 :(得分:1)

抛出异常导致堆栈展开,这会产生一些性能影响(承认,现代托管环境已经改进)。在嵌套情况下仍然反复抛出和捕获异常将是一个坏主意。

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

答案 21 :(得分:1)

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

在您的情况下,如果调用Login(用户名,密码)之类的方法,如果用户名无效,则抛出UserNameNotValidException或PasswordNotCorrectException(如果密码不正确)确实是正确的。用户无法使用提供的参数登录(即,这是不可能的,因为它会违反身份验证),因此抛出异常。虽然我可能有两个Exceptions继承自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;
    }
}

答案 22 :(得分:1)

避免抛出异常的主要原因是抛出异常会产生很多开销。

下面的文章指出的一个例外是异常情况和错误。

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

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

答案 23 :(得分:1)

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

实际上,在上面的场景中,有一个参数来选择多种故障处理模式可能会有所帮助:永远不会抛出异常,只抛出通信错误的异常,或者在命令不返回的任何情况下抛出异常一个“成功”的迹象。

答案 24 :(得分:1)

for me当所需的技术或业务规则失败时,应抛出异常。 例如,如果一个汽车实体与4个轮胎的阵列相关联......如果一个轮胎或更多轮胎为空......一个例外应该被解雇“NotEnoughTiresException”,因为它可以被捕获在系统的不同级别并具有显着性通过记录意义。 此外,如果我们只是尝试流动控制零并防止汽车的实例化。 我们可能永远也找不到问题的根源,因为首先轮胎不应该是空的。

答案 25 :(得分:0)

最终,决定取决于使用异常处理或通过您自己的归属机制(如返回状态代码)来处理这样的应用程序级错误更有帮助。关于哪个更好,我认为没有一个强硬的规则,但我会考虑:

  • 谁在调用您的代码?这是某种公共API还是内部库?
  • 您使用的是哪种语言?例如,如果它是Java,则抛出(已检查)异常会给调用者带来明显的负担,以某种方式处理此错误情况,而不是可以忽略的返回状态。这可能是好事还是坏事。
  • 如何处理同一个应用程序中的其他错误情况?调用者不希望处理以特殊方式处理错误的模块,而不像系统中的任何其他方式。
  • 有问题的例程会出现多少问题,以及如何以不同的方式处理它们?考虑一系列处理不同错误的catch块和错误代码切换之间的区别。
  • 您是否有关于需要返回的错误的结构化信息?抛出异常会让您更​​好地放置此信息,而不仅仅是返回状态。

答案 26 :(得分:0)

有两类主要的例外:

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

答案 27 :(得分:0)

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

  1. 在异常候选发生后要运行的代码级别 - 也就是说,调用堆栈的多少层应该展开。您通常希望处理异常尽可能接近它发生的位置。对于用户名/密码验证,您通常会在同一代码块中处理失败,而不是让异常冒出来。因此异常可能不合适。 (OTOH,在三次登录尝试失败后,控制流程可能会转移到其他位置,此处可能有例外情况。)

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

    < / LI>

答案 28 :(得分:0)

您可以对该条件使用一些通用例外。对于例如ArgumentException意味着在方法的参数出现任何问题时使用(ArgumentNullException除外)。通常,您不需要像LessThanZeroException,NotPrimeNumberException等异常。想想您的方法的用户。她想要特别处理的条件数等于您的方法需要抛出的异常类型的数量。这样,您就可以确定具体的例外情况。

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

答案 29 :(得分:0)

“PasswordNotCorrectException”不是使用异常的好例子。用户得到他们的密码是错误的,所以它几乎不是恕我直言。你甚至可能从它恢复,显示一个很好的错误信息,所以这只是一个有效性检查。

未处理的异常将最终停止执行 - 这很好。如果您返回false,null或错误代码,则必须自己处理程序的状态。如果您忘记检查某处的条件,您的程序可能会继续运行错误的数据,并且您可能很难搞清楚发生了什么以及

当然,你可能会在空的catch语句中引起同样的问题,但至少发现它们更容易,并且不需要你理解逻辑。

所以作为经验法则:

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

答案 30 :(得分:-1)

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

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
  }

}

答案 31 :(得分:-1)

如果您希望在返回值中表达两个或更多错误状态,请创建新的异常。

答案 32 :(得分:-2)

以下是我的建议:

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

在我看来,如果可以用“亲切,有礼貌”的方式处理某些事情(这意味着如果我们可以“通过使用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异常),则必须抛出异常。< / p>