首页 文章

什么时候抛出异常?

提问于
浏览
383

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

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

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

30 回答

  • 0

    因为他们不是一个例外情况 . 例外情况应该是一种非常罕见的事情 . {1477111_类型情况 .

  • 1

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

    • 使用例外来通知不应忽略的事项 .

    • 如果可以在本地处理错误,请不要使用异常

    • 确保异常与其他例程处于相同的抽象级别 .

    • 应该为 truly exceptional 保留例外情况 .

  • 1

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

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

    示例2:假设我有另一个检查List <>的函数,如果长度大于50则返回true,如果长度小则返回false . 这个函数问了这个问题,但是这个问题做了一个假设 - 假设给出的对象是一个列表 . 如果我把它交给NULL,那么这个假设就是假的 . 在这种情况下,如果函数返回true或false,则它会破坏自己的规则 . 该功能无法返回任何内容,并声称它正确回答了问题 . 所以它不会返回 - 它会引发异常 .

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

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

  • 15

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

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

  • 278

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

  • 34

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

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

  • 13

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

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

  • 24

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


    你会看到很多建议,包括在这个问题的答案中,你应该只在_1477113情况下抛出异常 . 这似乎是表面上合理的,但是有缺陷的建议,因为它用另一个主观问题("what is exceptional")取代了一个问题("when should I throw an exception") . 相反,请遵循Herb Sutter的建议(对于C,可在Dr Dobbs article When and How to Use Exceptions中找到,也可以在他与Andrei Alexandrescu,C编码标准的书中):如果且仅当

    • 不满足前提条件(通常使下列其中一项不可能)或

    • 替代方案将无法满足后置条件或

    • 替代方案将无法保持不变量 .

    为什么这样更好?它是否用几个关于先决条件,后置条件和不变量的问题取代了这个问题?由于几个相关的原因,这更好 .

    • 前置条件,后置条件和不变量是我们程序(其内部API)的设计特征,而 throw 的决定是实现细节 . 它迫使我们记住,我们必须分别考虑设计及其实现,而我们在实现方法时的工作是产生满足设计约束的东西 .

    • 它迫使我们根据前提条件,后置条件和不变量进行思考,这是我们方法的调用者应该做出的唯一假设,并且被精确地表达,使我们程序的组件之间能够松散耦合 .

    • 松散耦合允许我们在必要时重构实现 .

    • 后置条件和不变量是可测试的;它导致代码可以很容易地进行单元测试,因为后置条件是我们的单元测试代码可以检查(断言)的谓词 .

    • 在后置条件方面的思考自然会产生一种作为后置条件成功的设计,这是使用例外的自然风格 . 程序的正常("happy")执行路径是线性布局的,所有错误处理代码都移动到 catch 子句 .

  • 5

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

    使用例外的原因:

    • 常见情况的代码流更清晰

    • 可以将复杂的错误信息作为对象返回(虽然这也可以通过引用传递的错误"out"参数来实现)

    • 语言通常提供一些工具来管理异常情况下的整洁清理(在Java中尝试/最终,在C#中使用,在C中使用RAII)

    • 如果没有抛出异常,执行有时可能比检查返回代码更快

    • 在Java中,必须声明或捕获已检查的异常(尽管这可能是一个原因)

    不使用例外的原因:

    • 有时,如果错误处理很简单,那就太过分了

    • 如果未记录或声明异常,则可能通过调用代码来解决它们,这可能比调用代码忽略返回代码更糟糕(应用程序退出与静默失败 - 更糟糕的可能取决于方案)

    • 在C中,使用异常的代码必须是异常安全的(即使你不抛出或捕获它们,但间接调用throw函数)

    • 在C中,很难判断函数何时可能抛出,因此如果使用它们,你必须对异常安全感到偏执

    • 与检查返回标志相比,抛出和捕获异常通常要贵得多

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

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

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

  • 1

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

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

  • 2

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

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

  • 2

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

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

  • 5

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

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

  • 0

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

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

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

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

  • 1

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

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

  • 0

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

  • 561

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

  • 23

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

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

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

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

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

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

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

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

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

  • 1

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

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

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

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

  • 2

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

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

  • 1

    我有三种类型的条件 .

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

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

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

    希望这可以帮助

  • 10

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

  • 3

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

    在我看来,根本问题应该是,如果条件发生,是否会期望调用者想要继续正常的程序流程 . 如果你不知道,要么有单独的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()例程抛出异常会更有帮助 .

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

  • 0

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

  • 1

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

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

  • 2

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

    • 谁在调用你的代码?这是某种公共API还是内部库?

    • 您使用的是哪种语言?例如,如果它是Java,则抛出(已检查)异常会给调用者带来明显的负担,以某种方式处理此错误情况,而不是可以忽略的返回状态 . 这可能是好事也可能是坏事 .

    • 如何处理同一应用程序中的其他错误条件?调用者不希望处理以特殊方式处理错误的模块,这与系统中的任何其他方式不同 .

    • 有问题的例程会出现多少问题,以及如何以不同的方式处理它们?考虑一系列处理不同错误的catch块和错误代码切换之间的区别 .

    • 您是否有关于需要返回的错误的结构化信息?抛出异常可以让您更好地放置此信息,而不仅仅是返回状态 .

  • 56

    有两个主要的例外类别:

    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
    
  • 3

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

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

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

  • 2

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

    未处理的异常将最终停止执行 - 这很好 . 如果你自己陈述一切 . 如果您忘记检查某个地方的情况,您的程序可能会继续运行错误的数据,并且您可能很难弄清楚发生了什么以及发生了什么 .

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

    所以作为经验法则:

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

相关问题