错误变量是反模式还是好的设计?


44

为了处理不应停止执行的几种可能的错误,我有一个error变量,客户端可以检查并使用该变量引发异常。这是反模式吗?有没有更好的方法来解决这个问题?有关此操作的示例,您可以查看PHP的mysqli API。假定正确处理了可见性问题(访问器,公共和私有范围,是类中的变量还是全局变量?)。


6
这是什么try/ catch存在。此外,你可以把你的try/ catch更进一步的堆栈在处理它(允许的担忧更大的分离)更合适的位置。
jpmc26 2014年

注意事项:如果要使用基于异常的处理,并且遇到异常,则不想向用户显示过多信息。使用诸如Elmah或Raygun.io之类的错误处理程序来拦截它并向用户显示一般错误消息。切勿向用户显示堆栈跟踪或特定的错误消息,因为他们会泄露有关应用程序内部工作的信息,这些信息可能会被滥用。
Nzall 2014年

4
@Nate您的建议仅适用于用户完全不受信任的安全性至关重要的应用程序。模糊的错误消息本身就是一种反模式。在未经用户明确同意的情况下,通过网络发送错误报告也是如此。
piedar 2014年

3
:@piedar我创建一个单独的问题,其中该可以更自由地讨论programmers.stackexchange.com/questions/245255/...
Nzall

26
API设计的一个普遍原则是带给您无穷无尽的印象,即始终查看PHP在做什么,然后做完全相反的事情。
菲利普2014年

Answers:


65

如果一种语言固有地支持异常,则最好抛出异常,并且如果客户不希望异常导致失败,则客户端可以捕获异常。实际上,代码的客户端会期望异常,并且会遇到许多错误,因为它们将不检查返回值。

如果可以选择,使用异常有很多优点。

留言内容

异常包含用户可读的错误消息,开发人员可以将其用于调试,甚至在需要时显示给用户。如果使用方代码无法处理该异常,则它始终可以记录该异常,以便开发人员可以浏览日志,而不必停止其他任何跟踪以找出返回值,并将其映射到表中以找出返回值。实际例外。

使用返回值,无法轻松提供其他信息。某些语言将支持进行方法调用以获取最后的错误消息,因此可以消除这种担忧,但这需要调用方进行额外的调用,有时还需要访问带有此信息的“特殊对象”。

对于异常消息,我提供了尽可能多的上下文,例如:

无法为用户“ bar”检索名称为“ foo”的策略,该策略已在用户的配置文件中引用。

将此与返回代码-85进行比较。您想要哪一个?

调用栈

异常通常还具有详细的调用堆栈,这些堆栈可以帮助您越来越快地调试代码,如果需要,也可以由调用代码记录。这使开发人员通常可以将问题精确定位到确切的位置,因此功能非常强大。再一次将其与具有返回值(例如-85、101、0等)的日志文件进行比较,您希望使用哪一个?

失败快速偏向方法

如果某个方法在失败的地方被调用,它将引发异常。调用代码必须显式抑制该异常,否则它将失败。我发现这实际上是令人惊奇的,因为在开发和测试(甚至在生产中)期间,代码会快速失败,从而迫使开发人员对其进行修复。对于返回值,如果错过了对返回值的检查,则错误将被静默忽略,并且错误会在意外的地方出现,通常会花费更高的调试和修复成本。

包装和展开异常

可以将异常包装在其他异常中,然后根据需要进行包装。例如,您的代码可能会抛出ArgumentNullException调用代码可能换行的内容,UnableToRetrievePolicyException因为该操作在调用代码中失败了。尽管可能会向用户显示与我上面提供的示例类似的消息,但某些诊断代码可能会取消包装异常并发现ArgumentNullException引起了该问题,这意味着这是使用者代码中的编码错误。然后,这可能会发出警报,以便开发人员可以修复代码。这种高级方案很难通过返回值来实现。

代码简单

这一点很难解释,但是我通过这种编码学到了返回值和异常。使用返回值编写的代码通常会进行调用,然后对返回值进行一系列检查。在某些情况下,它将调用另一个方法,现在将对来自该方法的返回值进行另一系列检查。除了例外,在大多数情况下(即使不是全部),例外处理也要简单得多。您有一个try / catch / finally块,运行时会尽力执行finally块中的代码以进行清理。甚至嵌套的try / catch / finally块也比嵌套的if / else和来自多个方法的关联返回值相对容易实现和维护。

结论

如果您使用的平台支持异常(例如Java或.NET),那么您绝对应该假定除了抛出异常外别无其他方法,因为这些平台都有抛出异常的准则,并且您的客户会期望所以。如果我正在使用您的库,那么我不会费心检查返回值,因为我希望会引发异常,这就是这些平台的本质。

但是,如果使用的是C ++,则确定起来将更具挑战性,因为已经存在带有返回码的大型代码库,并且大量开发人员已调整为返回值而不是异常(例如Windows充斥着HRESULT) 。此外,在许多应用程序中,它也可能是一个性能问题(或至少被认为是)。


5
Windows从C ++函数返回HRESULT值,以维护其公共API中的C兼容性(并且因为尝试跨边界封送异常会陷入一个痛苦的世界)。如果要编写应用程序,请不要盲目遵循操作系统的模型。
科迪·格雷

2
我唯一要添加的就是提到松散耦合。异常使您可以在最合适的位置处理很多意外情况。例如,在一个Web应用程序中,您希望针对代码未准备好的任何异常返回500 ,而不是使该Web应用程序崩溃。因此,您需要在代码的顶部(或在框架中)捕获所有内容。桌面GUI中也存在类似情况。但是,您也可以在代码的不同位置放置不太通用的处理程序,以适合于当前尝试过程的方式处理各种失败情况。
jpmc26 2014年

2
@TrentonMaki如果您在谈论C ++构造函数的错误,最好的答案是在这里:parashift.com/c ++- faq-lite/ctors-can-throw.html。简而言之,请抛出一个异常,但请记住要先清除潜在的泄漏。我不知道其他任何语言,从构造函数中直接抛出都是不好的事情。我认为大多数API用户都希望捕获异常而不是检查错误代码。
Ogre Psalm33,2014年

3
“使用返回值很难实现这种高级方案。” 你当然可以!您要做的就是创建一个ErrorStateReturnVariable超类,它的一个属性是InnerErrorState(是的一个实例ErrorStateReturnVariable),实现子类的属性可以设置为显示一连串的错误……哦,等等。:p
Brian S

5
仅仅因为一种语言支持例外并不能使它们成为灵丹妙药。异常会引入隐藏的执行路径,因此需要适当地控制其影响。尝试/捕获很容易添加,但是要正确实现恢复非常困难,...
Matthieu M.14年

21

错误变量是诸如C之类的语言的遗物,在C语言中没有例外。今天,除了编写可从C程序(或类似语言,无异常处理)中使用的库时,应避免使用它们。

当然,如果您有一种错误类型,可以将其更好地分类为“警告”(=您的库可以提供有效的结果,并且调用方认为警告不重要,则可以忽略该警告),然后以表格形式显示状态指示器即使在带有例外的语言中,变量的意义也可以说得通。但是要当心。该库的调用者倾向于忽略此类警告,即使它们不应这样做。因此,在将这种构造引入您的lib之前,请三思。


1
感谢您的解释!您的答案+ Omer Iqbal回答了我的问题。
Mikayla Maki

处理“警告”的另一种方法是默认情况下引发异常,并具有某种可选标志以阻止引发异常。
Cyanfish

1
@Cyanfish:是的,但是必须注意不要过度设计这样的东西,尤其是在制作库时。比2、3或更多更好地提供一种简单而有效的警告机制。
布朗

仅当遇到故障,未知或不可恢复的情况时(特殊情况),才应引发异常。构造异常时,您应该预期会对性能产生影响
Gusdor 2014年

@Gusdor:绝对的,这就是为什么典型的“警告”默认情况下不应恕我直言引发异常的原因。但这还取决于抽象水平。有时,从调用者的角度来看,服务或库根本无法决定是否应将异常事件视为异常。就个人而言,在这种情况下,我更喜欢lib只是设置一个警告指示器(无异常),让调用者测试该标志并在他认为适当的情况下抛出异常。这就是我在上面写道“最好在库中提供一种警告机制”时所想到的。
布朗

20

有多种方法可以发出错误信号:

  • 要检查的错误变量:CGo,...
  • 例外:JavaC#,...
  • 一个“条件”处理程序:Lisp(仅?),...
  • 多态返回:HaskellMLRust,...

错误变量的问题是很容易忘记检查。

异常的问题是创建了隐藏的执行路径,尽管try / catch很容易编写,但是确保catch子句中的正确恢复确实很难实现(类型系统/编译器不提供支持)。

条件处理程序的问题在于它们的组合不好:如果您具有动态代码执行(虚拟函数),则无法预测应处理哪些条件。此外,如果可以在多个地方提出相同的条件,则不能说每次都可以应用统一的解决方案,并且很快就会变得混乱。

Either a b到目前为止,多态返回(在Haskell中)是我最喜欢的解决方案:

  • 明确的:没有隐藏的执行路径
  • 明确的:在函数类型签名中有完整记录(毫不奇怪)
  • 难以忽视:您必须进行模式匹配才能获得所需的结果,并处理错误情况

唯一的问题是,它们可能会导致过度检查。使用它们的语言有习惯用法来链接使用它们的函数的调用,但可能仍需要更多的键入/混乱。在Haskell,这将是单子 ; 但是,这比听起来要可怕的多,请参阅《面向铁路的编程》


1
好答案!我希望可以得到处理错误的方法列表。
Mikayla Maki 2014年

啊!我看过几次“错误变量的问题是很容易忘记检查”。例外的问题是很容易忘记抓住它。然后您的应用程序崩溃。您的老板不想为修复它付费,但是您的客户停止使用您的应用程序,因为他们对崩溃感到沮丧。所有这些都是因为如果返回并忽略错误代码,这不会影响程序执行。我见过的人唯一一次忽略错误代码的时间就是那没什么大不了的。链中更高的代码知道出了点问题。
Dunk 2014年

1
@Dunk:我也不认为我的回答为例外道歉;但是,这很可能取决于您编写的代码类型。我的个人工作经验倾向于在出现错误的情况下失败的系统,因为静默数据损坏更严重(且未被发现),并且我处理的数据对客户端有价值(当然,这也意味着紧急修复)。
Matthieu M.

这不是静默数据损坏的问题。这是一个了解您的应用程序以及知道何时需要在何处验证操作是否成功的问题。在许多情况下,确定和处理该决定可能会延迟。什么时候我必须处理一个失败需要的失败操作,这不应该由其他人来告诉我,这是异常的要求。这是我的应用程序,我知道我想在何时何地处理任何相关问题。如果人们编写的应用程序可能破坏数据,那么那将是一件非常糟糕的事情。编写崩溃的应用程序(我看到很多)也做得不好。
Dunk 2014年

12

我认为这很糟糕。我目前正在重构使用返回值而不是异常的Java应用程序。尽管您可能根本不使用Java,但我认为这仍然适用。

您最终得到这样的代码:

String result = x.doActionA();
if (result != null) {
  throw new Exception(result);
}
result = x.doActionB();
if (result != null) {
  throw new Exception(result);
}

或这个:

if (!x.doActionA()) {
  throw new Exception(x.getError());
}
if (!x.doActionB()) {
  throw new Exception(x.getError());
}

我宁愿让操作本身抛出异常,所以最终得到类似以下内容:

x.doActionA();
x.doActionB();

您可以将其包装在try-catch中,并从异常中获取消息,也可以选择忽略该异常,例如,当您删除可能已经消失的内容时。如果有,它还会保留堆栈跟踪。方法本身也变得更加容易。他们没有处理异常本身,而是抛出错误。

当前(可怕)代码:

private String doActionA() {
  try {
    someOperationThatCanGoWrong1();
    someOperationThatCanGoWrong2();
    someOperationThatCanGoWrong3();
    return null;
  } catch(Exception e) {
    return "Something went wrong!";
  }
}

新增和改进:

private void doActionA() throws Exception {
  someOperationThatCanGoWrong1();
  someOperationThatCanGoWrong2();
  someOperationThatCanGoWrong3();
}

保留Strack跟踪,并且在异常情况下可以使用该消息,而不是无用的“出了点问题!”。

当然,您可以提供更好的错误消息,应该这样做。但是这篇文章在这里是因为我正在使用的当前代码很痛苦,您不应该这样做。


1
不过,有一个问题是,在某些情况下,您的“新的和改进的”会失去异常最初发生的位置的上下文。例如,在doActionA()的“当前(可怕的)版本”中,catch子句可以访问实例变量和来自封闭对象的其他信息,以提供更有用的消息。
InformedA 2014年

1
是的,但是目前还没有发生。而且,您始终可以在doActionA中捕获该异常,并将其包装在带有状态消息的另一个异常中。然后,您将仍然拥有堆栈跟踪有用的消息。 throw new Exception("Something went wrong with " + instanceVar, ex);
mrjink 2014年

我同意,在您的情况下可能不会发生。但是您不能“始终”将信息放入doActionA()中。为什么?doActionA()的调用方可能是唯一包含您需要包括的信息的调用方。
通知A

2
因此,请调用方在处理异常时将其包括在内。不过,这同样适用于原始问题。现在您无能为力,无法使用异常处理,这会导致代码更简洁。与返回的布尔值或错误消息相比,我更喜欢使用异常。
mrjink 2014年

您通常做出错误的假设,即可以以相同的方式处理和清除在任何“ someOperation”中发生的异常。现实生活中发生的事情是您需要捕获并处理每个操作的异常。因此,您不仅会像示例中那样抛出异常。而且,使用异常然后最终会创建一堆嵌套的try-catch块或一系列try-catch块。这通常会使代码的可读性大大降低。我没有反对例外,但是我针对特定情况使用了适当的工具。例外只是1种工具。
Dunk 2014年

5

“为了处理可能发生的几种错误,不应停止执行,”

如果您的意思是错误不应阻止当前函数的执行,而应以某种方式报告给调用者-那么您有几个未真正提及的选项。这种情况实际上是警告而不是错误。投掷/返回不是选项,因为它会终止当前功能。单个错误消息的参数或返回仅允许最多发生这些错误之一。

我使用的两种模式是:

  • 错误/警告集合,可以传入或保留为成员变量。您将内容附加到其中并继续进行处理。我个人并不真的喜欢这种方法,因为我认为它无法使呼叫者受益。

  • 传入错误/警告处理程序对象(或将其设置为成员变量)。每个错误都调用处理程序的成员函数。这样,呼叫者可以决定如何处理此类非终止错误。

您传递给这些集合/处理程序的内容应包含足够的上下文,以便可以“正确”处理错误-字符串通常太少,通常将它传递给Exception的某些实例是明智的-但有时会皱眉(因为滥用Exception) 。

使用错误处理程序的典型代码可能如下所示

class MyFunClass {
  public interface ErrorHandler {
     void onError(Exception e);
     void onWarning(Exception e);
  }

  ErrorHandler eh;

  public void canFail(int i) {
     if(i==0) {
        if(eh!=null) eh.onWarning(new Exception("canFail shouldn't be called with i=0"));
     }
     if(i==1) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=1 is fatal");
        throw new RuntimeException("canFail called with i=2");
     }
     if(i==2) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=2 is an error, but not fatal"));
     }
  }
}

3
+1表示用户需要警告而不是错误。可能值得一提的是Python的warnings软件包,它为这个问题提供了另一种模式。
James_pic

感谢您的出色回答!这是我想看到的更多内容,是用于处理错误的其他模式,而传统的try / catch可能不足。
Mikayla Maki 2014年

值得一提的是,传入的错误回调对象可以在某些错误或警告发生时引发异常(实际上,这可能是默认行为),但它可能对询问的方式也很有用。调用函数做某事。例如,“句柄分析错误”方法可能会给调用方一个应认为已返回分析的值。
2014年

5

只要使用其他所有人使用的模式,使用此模式或该模式通常没有任何错误。在Objective-C开发中,更可取的模式是传递一个指针,在该指针处调用的方法可以存放NSError对象。保留了一些异常以保留编程错误并导致崩溃(除非您有Java或.NET程序员编写了他们的第一个iPhone应用程序)。而且效果很好。


4

这个问题已经回答了,但我无能为力。

您真的不能期望Exception为所有用例提供解决方案。有人打吗?

在某些情况下,例外不是全部结束,而是全部结束,例如,如果某个方法收到一个请求并负责验证所有传递的字段,那么不仅是第一个,您还必须认为应该可以在多个字段中指出错误的原因。还应该指出验证的性质是否阻止用户走得更远。例如,密码不强。您可以向用户显示一条消息,指示输入的密码不是很强,但足够强。

您可能会争辩说,所有这些验证都可能在验证模块的末尾作为异常抛出,但它们的名称以外的任何地方都将是错误代码。

因此,这里的教训是:异常以及错误代码都有它们的位置。明智地选择。


我认为这暗示着不良的设计。带有参数的方法应该能够处理或不处理-中间什么也没有。您的示例应Validator在相关方法(或其背后的对象)中注入(接口)。根据注入Validator的方式,该方法将使用错误的密码进行操作-否则不会进行。WeakValidator如果用户WeakPasswordException在最初尝试的抛出a之后,是否要求用户输入周围的代码,则可以尝试StrongValidator
jhr 2014年

啊,但是我没有说这不能是接口或验证器。我什至没有提到JSR303。而且,如果您仔细阅读,我一定不会说弱密码,而是说它不是很强。弱密码将是阻止流量并要求用户提供强密码的原因。
Alexandre Santos 2014年

而您会使用中等强度但不是真的弱密码呢?您会中断该流程,并向用户显示一条消息,指示输入的密码不是很安全。所以有一个MiddlyStrongValidator或类似的东西。而且,如果这并没有真正打断您的流程,则Validator必须事先调用该流程,即在用户仍在输入密码(或类似密码)的情况下进行流程之前。但是,首先,验证并不是所讨论方法的一部分。:)毕竟可能是个口味问题
jhr 2014年

@jhr在我编写的验证器中,通常将创建一个AggregateException(或类似的ValidationException),并将每个验证问题的特定异常放入InnerExceptions中。例如,它可能是BadPasswordException:“用户密码小于最小长度6”或MandatoryFieldMissingException:“必须为用户提供名字”等。这不等同于错误代码。所有这些消息都可以以一种用户可以理解的方式显示给用户,如果NullReferenceException抛出a消息,那么我们将得到一个错误。
Omer Iqbal 2014年

4

在某些情况下,错误代码优于异常。

如果尽管有错误您的代码仍然可以继续,但是需要报告,那么异常是一个糟糕的选择,因为异常会终止流程。例如,如果您正在读取一个数据文件,并且发现它包含一些非终端的坏数据,则最好读取文件的其余部分并报告错误,而不是直接失败。

其他答案已经涵盖了为什么通常应该优先使用异常而不是错误代码。


如果需要记录警告或其他内容,就这样。但是,如果发生错误“需要报告”(我假设您的意思是向用户报告),则无法保证周围的代码将读取您的返回代码并实际进行报告。
jhr 2014年

不,我的意思是将其报告给呼叫者。与异常一样,决定用户是否需要了解错误是呼叫者的责任。
杰克·艾德利2014年

1
@jhr:什么时候可以保证?班级合同可以规定客户承担某些责任;如果客户遵守合同,他们将执行合同要求的事情。否则,任何后果将是客户端代码的错误。如果要防止客户端意外错误并控制由序列化方法返回的类型,则可以使其包含“未确认的可能损坏”标志,并且不允许客户端在不调用AcknowledgePossibleCorruption方法的情况下从中读取数据。 。
supercat

1
...但是拥有一个对象类来保存有关问题的信息可能比抛出异常或返回通过失败错误代码更有用。取决于应用程序以适当的方式使用该信息(例如,在加载文件“ Foo”时,通知用户数据可能不可靠,并在保存时提示用户选择新名称)。
超级猫

1
如果使用异常,则有一个保证:如果您不捕获它们,它们会抛出更高的值-最坏的情况是到达UI。如果您使用没人读的返回码,则无法保证。当然,如果您想使用该API,请遵循该API。我同意!但不幸的是,这里仍有出错的余地……
jhr 2014年

2

当异常不合适时,不使用异常绝对没有错。

当不应该中断代码执行时(例如,对可能包含多个错误的用户输入进行操作,例如要编译的程序或要处理的表单),我发现收集错误变量中的错误,例如has_errors并且error_messages确实比抛出错误要优雅得多第一个错误发生异常。它允许查找用户输入中的所有错误,而不必强迫用户重新提交。


有趣的问题。我认为我的问题和我的理解的问题是不清楚的术语。您所描述的并不是什么例外,但这是一个错误。我们应该怎么称呼它?
Mikayla Maki 2014年

1

在某些动态编程语言中,可以同时使用错误值异常处理。这是通过返回未抛出异常的对象代替普通的返回值来完成的,该对象可以像错误值一样进行检查,但是如果不检查,它将引发异常。

Perl 6中,它是通过来完成的fail,如果no fatal;with作用域返回一个特殊的未抛出异常Failure对象。

Perl 5中,您可以使用Contextual :: Return来执行此操作return FAIL


-1

除非有非常具体的说明,否则我认为为验证使用错误变量是一个坏主意。目的似乎是为了节省验证时间(您可以只返回变量值)

但是,如果您进行了任何更改,则无论如何都必须重新计算该值。我不能说更多关于停止和异常抛出的信息。

编辑:我没有意识到这是软件范式的问题,而不是具体情况。

让我进一步阐明我的一个具体案例中的观点,在这个案例中我的答案很有意义

  1. 我有实体对象的集合
  2. 我有使用这些实体对象的过程样式Web服务

有两种错误:

  1. 服务层中发生处理时的错误
  2. 错误,因为实体对象中存在不一致

在服务层中,别无选择,只能使用Result对象作为包装器,这是错误变量的等效项。可以通过对协议(如http)的服务调用来模拟异常,但这绝对不是一件好事。我不是在谈论这种错误,也不认为这是在此问题中提出的那种错误。

我在考虑第二种错误。我的答案是关于第二种错误。在实体对象中,有很多选择供我们选择,其中一些是

  • 使用验证变量
  • 当设置器中的字段设置不正确时,立即引发异常

使用验证变量与为每个实体对象使用单一验证方法相同。尤其是,用户可以通过以下方式设置值:将设置器保持为纯设置器,没有副作用(这通常是一种好习惯),或者可以将验证合并到每个设置器中,然后将结果保存到验证变量中。这样的好处是可以节省时间,将验证结果缓存到验证变量中,这样,当用户多次调用validate()时,就无需进行多次验证。

在这种情况下,最好的办法是使用单一验证方法,甚至不使用任何验证来缓存验证错误。这有助于将设置者保持为正确的设置者。


我明白你在说什么。有趣的方法。我很高兴这是答案集中的一部分。
Mikayla Maki
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.