为了处理不应停止执行的几种可能的错误,我有一个error
变量,客户端可以检查并使用该变量引发异常。这是反模式吗?有没有更好的方法来解决这个问题?有关此操作的示例,您可以查看PHP的mysqli API。假定正确处理了可见性问题(访问器,公共和私有范围,是类中的变量还是全局变量?)。
为了处理不应停止执行的几种可能的错误,我有一个error
变量,客户端可以检查并使用该变量引发异常。这是反模式吗?有没有更好的方法来解决这个问题?有关此操作的示例,您可以查看PHP的mysqli API。假定正确处理了可见性问题(访问器,公共和私有范围,是类中的变量还是全局变量?)。
Answers:
如果一种语言固有地支持异常,则最好抛出异常,并且如果客户不希望异常导致失败,则客户端可以捕获异常。实际上,代码的客户端会期望异常,并且会遇到许多错误,因为它们将不检查返回值。
如果可以选择,使用异常有很多优点。
留言内容
异常包含用户可读的错误消息,开发人员可以将其用于调试,甚至在需要时显示给用户。如果使用方代码无法处理该异常,则它始终可以记录该异常,以便开发人员可以浏览日志,而不必停止其他任何跟踪以找出返回值,并将其映射到表中以找出返回值。实际例外。
使用返回值,无法轻松提供其他信息。某些语言将支持进行方法调用以获取最后的错误消息,因此可以消除这种担忧,但这需要调用方进行额外的调用,有时还需要访问带有此信息的“特殊对象”。
对于异常消息,我提供了尽可能多的上下文,例如:
无法为用户“ bar”检索名称为“ foo”的策略,该策略已在用户的配置文件中引用。
将此与返回代码-85进行比较。您想要哪一个?
调用栈
异常通常还具有详细的调用堆栈,这些堆栈可以帮助您越来越快地调试代码,如果需要,也可以由调用代码记录。这使开发人员通常可以将问题精确定位到确切的位置,因此功能非常强大。再一次将其与具有返回值(例如-85、101、0等)的日志文件进行比较,您希望使用哪一个?
失败快速偏向方法
如果某个方法在失败的地方被调用,它将引发异常。调用代码必须显式抑制该异常,否则它将失败。我发现这实际上是令人惊奇的,因为在开发和测试(甚至在生产中)期间,代码会快速失败,从而迫使开发人员对其进行修复。对于返回值,如果错过了对返回值的检查,则错误将被静默忽略,并且错误会在意外的地方出现,通常会花费更高的调试和修复成本。
包装和展开异常
可以将异常包装在其他异常中,然后根据需要进行包装。例如,您的代码可能会抛出ArgumentNullException
调用代码可能换行的内容,UnableToRetrievePolicyException
因为该操作在调用代码中失败了。尽管可能会向用户显示与我上面提供的示例类似的消息,但某些诊断代码可能会取消包装异常并发现ArgumentNullException
引起了该问题,这意味着这是使用者代码中的编码错误。然后,这可能会发出警报,以便开发人员可以修复代码。这种高级方案很难通过返回值来实现。
代码简单
这一点很难解释,但是我通过这种编码学到了返回值和异常。使用返回值编写的代码通常会进行调用,然后对返回值进行一系列检查。在某些情况下,它将调用另一个方法,现在将对来自该方法的返回值进行另一系列检查。除了例外,在大多数情况下(即使不是全部),例外处理也要简单得多。您有一个try / catch / finally块,运行时会尽力执行finally块中的代码以进行清理。甚至嵌套的try / catch / finally块也比嵌套的if / else和来自多个方法的关联返回值相对容易实现和维护。
结论
如果您使用的平台支持异常(例如Java或.NET),那么您绝对应该假定除了抛出异常外别无其他方法,因为这些平台都有抛出异常的准则,并且您的客户会期望所以。如果我正在使用您的库,那么我不会费心检查返回值,因为我希望会引发异常,这就是这些平台的本质。
但是,如果使用的是C ++,则确定起来将更具挑战性,因为已经存在带有返回码的大型代码库,并且大量开发人员已调整为返回值而不是异常(例如Windows充斥着HRESULT) 。此外,在许多应用程序中,它也可能是一个性能问题(或至少被认为是)。
ErrorStateReturnVariable
超类,它的一个属性是InnerErrorState
(是的一个实例ErrorStateReturnVariable
),实现子类的属性可以设置为显示一连串的错误……哦,等等。:p
错误变量是诸如C之类的语言的遗物,在C语言中没有例外。今天,除了编写可从C程序(或类似语言,无异常处理)中使用的库时,应避免使用它们。
当然,如果您有一种错误类型,可以将其更好地分类为“警告”(=您的库可以提供有效的结果,并且调用方认为警告不重要,则可以忽略该警告),然后以表格形式显示状态指示器即使在带有例外的语言中,变量的意义也可以说得通。但是要当心。该库的调用者倾向于忽略此类警告,即使它们不应这样做。因此,在将这种构造引入您的lib之前,请三思。
有多种方法可以发出错误信号:
错误变量的问题是很容易忘记检查。
异常的问题是创建了隐藏的执行路径,尽管try / catch很容易编写,但是确保catch子句中的正确恢复确实很难实现(类型系统/编译器不提供支持)。
条件处理程序的问题在于它们的组合不好:如果您具有动态代码执行(虚拟函数),则无法预测应处理哪些条件。此外,如果可以在多个地方提出相同的条件,则不能说每次都可以应用统一的解决方案,并且很快就会变得混乱。
Either a b
到目前为止,多态返回(在Haskell中)是我最喜欢的解决方案:
唯一的问题是,它们可能会导致过度检查。使用它们的语言有习惯用法来链接使用它们的函数的调用,但可能仍需要更多的键入/混乱。在Haskell,这将是单子 ; 但是,这比听起来要可怕的多,请参阅《面向铁路的编程》。
我认为这很糟糕。我目前正在重构使用返回值而不是异常的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跟踪,并且在异常情况下可以使用该消息,而不是无用的“出了点问题!”。
当然,您可以提供更好的错误消息,应该这样做。但是这篇文章在这里是因为我正在使用的当前代码很痛苦,您不应该这样做。
throw new Exception("Something went wrong with " + instanceVar, ex);
“为了处理可能发生的几种错误,不应停止执行,”
如果您的意思是错误不应阻止当前函数的执行,而应以某种方式报告给调用者-那么您有几个未真正提及的选项。这种情况实际上是警告而不是错误。投掷/返回不是选项,因为它会终止当前功能。单个错误消息的参数或返回仅允许最多发生这些错误之一。
我使用的两种模式是:
错误/警告集合,可以传入或保留为成员变量。您将内容附加到其中并继续进行处理。我个人并不真的喜欢这种方法,因为我认为它无法使呼叫者受益。
传入错误/警告处理程序对象(或将其设置为成员变量)。每个错误都调用处理程序的成员函数。这样,呼叫者可以决定如何处理此类非终止错误。
您传递给这些集合/处理程序的内容应包含足够的上下文,以便可以“正确”处理错误-字符串通常太少,通常将它传递给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"));
}
}
}
warnings
软件包,它为这个问题提供了另一种模式。
只要使用其他所有人使用的模式,使用此模式或该模式通常没有任何错误。在Objective-C开发中,更可取的模式是传递一个指针,在该指针处调用的方法可以存放NSError对象。保留了一些异常以保留编程错误并导致崩溃(除非您有Java或.NET程序员编写了他们的第一个iPhone应用程序)。而且效果很好。
这个问题已经回答了,但我无能为力。
您真的不能期望Exception为所有用例提供解决方案。有人打吗?
在某些情况下,例外不是全部结束,而是全部结束,例如,如果某个方法收到一个请求并负责验证所有传递的字段,那么不仅是第一个,您还必须认为应该可以在多个字段中指出错误的原因。还应该指出验证的性质是否阻止用户走得更远。例如,密码不强。您可以向用户显示一条消息,指示输入的密码不是很强,但足够强。
您可能会争辩说,所有这些验证都可能在验证模块的末尾作为异常抛出,但它们的名称以外的任何地方都将是错误代码。
因此,这里的教训是:异常以及错误代码都有它们的位置。明智地选择。
Validator
在相关方法(或其背后的对象)中注入(接口)。根据注入Validator
的方式,该方法将使用错误的密码进行操作-否则不会进行。WeakValidator
如果用户WeakPasswordException
在最初尝试的抛出a之后,是否要求用户输入周围的代码,则可以尝试StrongValidator
。
MiddlyStrongValidator
或类似的东西。而且,如果这并没有真正打断您的流程,则Validator
必须事先调用该流程,即在用户仍在输入密码(或类似密码)的情况下进行流程之前。但是,首先,验证并不是所讨论方法的一部分。:)毕竟可能是个口味问题
AggregateException
(或类似的ValidationException
),并将每个验证问题的特定异常放入InnerExceptions中。例如,它可能是BadPasswordException
:“用户密码小于最小长度6”或MandatoryFieldMissingException
:“必须为用户提供名字”等。这不等同于错误代码。所有这些消息都可以以一种用户可以理解的方式显示给用户,如果NullReferenceException
抛出a消息,那么我们将得到一个错误。
在某些情况下,错误代码优于异常。
如果尽管有错误您的代码仍然可以继续,但是需要报告,那么异常是一个糟糕的选择,因为异常会终止流程。例如,如果您正在读取一个数据文件,并且发现它包含一些非终端的坏数据,则最好读取文件的其余部分并报告错误,而不是直接失败。
其他答案已经涵盖了为什么通常应该优先使用异常而不是错误代码。
AcknowledgePossibleCorruption
方法的情况下从中读取数据。 。
当异常不合适时,不使用异常绝对没有错。
当不应该中断代码执行时(例如,对可能包含多个错误的用户输入进行操作,例如要编译的程序或要处理的表单),我发现收集错误变量中的错误,例如has_errors
并且error_messages
确实比抛出错误要优雅得多第一个错误发生异常。它允许查找用户输入中的所有错误,而不必强迫用户重新提交。
在某些动态编程语言中,可以同时使用错误值和异常处理。这是通过返回未抛出异常的对象代替普通的返回值来完成的,该对象可以像错误值一样进行检查,但是如果不检查,它将引发异常。
在Perl 6中,它是通过来完成的fail
,如果no fatal;
with作用域返回一个特殊的未抛出异常Failure
对象。
在Perl 5中,您可以使用Contextual :: Return来执行此操作return FAIL
。
除非有非常具体的说明,否则我认为为验证使用错误变量是一个坏主意。目的似乎是为了节省验证时间(您可以只返回变量值)
但是,如果您进行了任何更改,则无论如何都必须重新计算该值。我不能说更多关于停止和异常抛出的信息。
编辑:我没有意识到这是软件范式的问题,而不是具体情况。
让我进一步阐明我的一个具体案例中的观点,在这个案例中我的答案很有意义
有两种错误:
在服务层中,别无选择,只能使用Result对象作为包装器,这是错误变量的等效项。可以通过对协议(如http)的服务调用来模拟异常,但这绝对不是一件好事。我不是在谈论这种错误,也不认为这是在此问题中提出的那种错误。
我在考虑第二种错误。我的答案是关于第二种错误。在实体对象中,有很多选择供我们选择,其中一些是
使用验证变量与为每个实体对象使用单一验证方法相同。尤其是,用户可以通过以下方式设置值:将设置器保持为纯设置器,没有副作用(这通常是一种好习惯),或者可以将验证合并到每个设置器中,然后将结果保存到验证变量中。这样的好处是可以节省时间,将验证结果缓存到验证变量中,这样,当用户多次调用validate()时,就无需进行多次验证。
在这种情况下,最好的办法是使用单一验证方法,甚至不使用任何验证来缓存验证错误。这有助于将设置者保持为正确的设置者。
try
/catch
存在。此外,你可以把你的try
/catch
更进一步的堆栈在处理它(允许的担忧更大的分离)更合适的位置。