it-swarm.cn

什么时候抛出异常?

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

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

创建例外的任何指导或最佳实践?

395
Kwan Cheng

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

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

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

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

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

576
The Digital Gabeg

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

279
blowdart

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

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

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

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

34
EricSchaefer

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

25
japollock

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

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

23
Bjorn Reppen

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

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

16
Mitchel Sellers

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


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

  • 不满足前提条件(通常使下列其中一项不可能)或
  • 替代方案将无法满足后置条件或
  • 替代方案将无法保持不变。

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

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

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

使用例外的原因:

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

不使用例外的原因:

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

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

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

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

10
Robert

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

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

5
Shachar

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

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

5
Charles Graham

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

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

3
dclaysmith

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

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

3
goran

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

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

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

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

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

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

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

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

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

2
Petr Macek

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

2
Joe Skora

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

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

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

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

2
Gord

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

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

2
Jason Etheridge

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

2
Dan

我有三种类型的条件。

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

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

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

希望这可以帮助

1
Michael

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

1
Genjuro

简单的答案是,只要操作不可能(因为它会违反业务逻辑,因此应用程序OR)。如果调用了一个方法并且无法执行该方法的编写操作,则抛出异常。一个很好的例子是,如果无法使用提供的参数创建实例,构造函数将始终抛出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;
    }
}
1
core

在我看来,根本问题应该是,如果条件发生,是否会期望调用者想要继续正常的程序流程。如果你不知道,要么有单独的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 (“将tempfile复制到realfile”); 

人们会希望任何操作都不能中止整个序列。虽然可以检查每个操作以确保它成功,但是如果命令失败,让exchange_command()例程抛出异常会更有帮助。

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

1
supercat

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

下面的文章指出的一件事是异常是针对异常情况和错误。

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

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

1
Sam

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

1
anon

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

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

1
Arno

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

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

  2. 这个事件是您想要在错误日志中看到的吗?并非每个异常都写入错误日志,但是询问错误日志中的此条目是否有用是有用的 - 即,您将尝试对其执行某些操作,或者是您忽略的垃圾。

0
Mike Kantor

有两个主要的例外类别:

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
0
Crusty

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

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

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

所以作为经验法则:

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

0
DanMan

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

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

0
Serhat Ozgel

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

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