从您的方法返回异常是不好的做法

Posted

技术标签:

【中文标题】从您的方法返回异常是不好的做法【英文标题】:Is it bad practice to return Exceptions from your methods 【发布时间】:2010-11-01 16:16:37 【问题描述】:

我的同行编写的许多代码库方法都执行自己的错误处理,通常是通过捕获、通知和记录。

在这些情况下,方法返回一个布尔值,指示成功或失败。

但有时,如果方法失败,我希望调用代码知道原因,而返回布尔值是不够的。

解决这个问题的一种方法是在方法中保留错误处理,但使方法无效,并让方法抛出它们的异常。

但是,最近我养成了在适当的方法中返回异常的习惯,通常是在其他情况下可能无效的操作方法,但我想知道操作的状态。

返回 Exception 相对于 void 方法抛出 Exception 的优势在于,我认为它使其他程序员更容易使用您的库,并且,更重要的是,这意味着您可以安全地调用该方法而无需不必担心捕获异常

例如,如果方法只是 void,则程序员应该处理成功或失败可能不会立即显而易见。

但如果方法明确指定返回类型为 Exception,那么您就知道可以根据需要检查成功或失败。但这也意味着如果您不想捕获错误,则无需担心。

这有意义吗?也许我没有使用最好的例子,但一般来说,返回异常是否可以,或者有更好的模式吗?

更新

哇,压倒性的结果是不可能。我是这么想的。我必须说,这样做(返回异常)有点解决了一个问题,但确实感觉不对。

因此,从您的一些答案来看,这些特定场景(复杂类,或具有一个或多个外部依赖项(即 Web 服务)的类)的最佳解决方案似乎是自定义结果类?

更新:

我非常感谢所有意见,我正在阅读所有内容,并且我正在仔细考虑所有输入。

目前我更喜欢使用 void 方法,抛出异常,然后在外面捕获它们......这样更好吗?

【问题讨论】:

你们是否意识到 SO 社区刚刚拯救了安迪进入 TheDailyWTF 名人堂?这就是我如此喜欢的原因:我可以向数以千计的优秀开发人员询问并获得智慧,而不是搞砸大事。 erm....谢谢 Esteban...我猜 ;-) @andy:别担心,伙计。我之所以这么说,是因为我实际上做了比你刚刚考虑更糟糕的事情方式 @andy:另外,如果我被认为是一个傲慢的混蛋,我深表歉意。老实说,我想强调我如此喜欢的原因之一:它为我的培根节省了十亿次。 哈哈,不需要道歉! 【参考方案1】:

不,这完全是在贬低语言(技术,因为您只指定了 .NET)。

我也不同意它使您的 API 更易于其他人使用,任何已经学习并基本了解使用包含异常的模型进行编程的人都会发现使用您的代码更加困难。

如果将来您的方法需要返回一个值,会发生什么?如果有多个错误场景怎么办?如果您真的想了解更多关于该方法是如何进行的,那么您需要创建一个类来封装结果和您想要的任何其他状态信息。

【讨论】:

感谢agileJon,这就是我问的原因。注意说明原因,或为类似情况提出更好的模式。我特别写了这个问题,因为我对返回异常感到不舒服。我很乐意听取建议。 在这种情况下,我使用了一个存储异常或 T 的泛型类,这样我就编写了一个泛型类,它可以让我将任何结果或异常传回,而无需编写类对于每个可能有异常的函数返回调用。【参考方案2】:

如果你的意思是......

public Exception MyMethod( string foo )

   if( String.IsNullOrEmpty() )
   
      return new ArgumentNullException( "foo" );
   

...而不是...

public void MyMethod( string foo )

   if( String.IsNullOrEmpty() )
   
      throw new ArgumentNullException( "foo" )
   

那绝对不行,那样做是不行的。您将完全重新发明异常的目的并将其用作结果。这里有一些标准的best practices。

另一个不这样做的好理由是标准委托将不再匹配您的方法签名。因此,例如,您不能再在 MyMethod 上使用 Action<string>,而需要使用 Func<string,Exception>

@Andy,对于每条评论,回答太长以至于无法发表评论:不是真的。不要太在意来电者。无论如何,他的设计应该是防御性的。想想异常的语义……“除非有人知道如何解决这个问题,否则这个应用程序的执行将在这里停止。”如果你能解决问题,你应该解决它。如果你不能,你必须记录它并把它扔到下一个级别,因为他们可能确切地知道该怎么做。

你应该处理你能处理的东西,扔掉你不能处理的东西。根据定义,调用堆栈上的人比你拥有更广阔的世界观。您的应用程序需要具有处理异常并继续运行的能力。他们这样做的唯一方法是防御性编码,并将问题推送到更高级别的信息,看看是否可以做任何事情。

如果最终答案是“否”,那么将问题记录到它可以理解,做一个好公民并优雅地终止应用程序,并为新的一天战斗。不要自私,并尝试为您的来电者隐藏错误。保持防守,他也会这样做。 :)

查看Enterprise Library Exception handling block。它认为他们真正阐明了如何处理整个架构中的异常的伟大愿景。

【讨论】:

当然要注意那些应该是ArgumentNullException 实例......其次,有一个例外(har har)在某种程度上是允许的,那就是如果你碰巧有一个“异常生成器” ' 为你构造一个异常以便你可以抛出它的方法。 如果使用异常是最佳实践...为什么存在 tryCast 和 TryParse?我假设在内部他们不使用尝试捕获,只是良好的错误处理并传回成功或失败?在我看来,异常被发明来代表一个问题,抛出它们是一种使用它们的方法,就像传递或返回一样。当然,您可以只允许调用者通过布尔参数或方法的 tryX 版本来决定他们更喜欢哪种方式。 我还要补充一点;我认为理想情况下,应该编写代码以允许其他开发人员永远不需要捕获异常。当代码使用不正确时应该抛出异常,除了抛出正常的 ArgumentExceptions 类型之外,您应该创建一个新的异常类型并更好地解释抛出的原因。还要为 throw 添加 XML 注释... @jerryjvl:后来我确实想到了异常工厂案例,所以我同意。 :) @Maslow -- TryCast 和 TryParse 存在是因为在某些情况下,失败并不是这些操作的例外情况。示例:当从标准输入解析数字时,您应该预料到某些 luser 不小心将字母与他们输入的数字混合在一起。在其他情况下(例如,当它是您的程序先前写入磁盘本身的值时),由于无效字符而导致解析操作失败确实是例外。所以有时失败是一种特殊情况,有时则不是。这就是框架提供两种方法的原因。【参考方案3】:

我从未见过返回异常的方法。通常,该方法返回一个值,并且该方法可能抛出异常。但我从未见过返回 new System.InvalidOperationException("Logfile cannot be read-only"); 的代码比如……

所以要回答你的问题,我会说是的,这很糟糕......

【讨论】:

因为其他人没有这样做,所以本质上是坏事?呼吁大众化的谬误 如果说其他人在接管(或调用)您的代码时无法理解您所做的事情,这本身就是不好的。【参考方案4】:

您永远不应该返回异常。相反,如果您的函数足够复杂,请创建一个作为函数结果的类。例如,GetPatientList 方法返回 GetPatientListResult。它可以有一个 Success 的成员。例外是非常不常见的东西,例如文件在操作中消失,为函数设置无效或空参数,数据库关闭。此外,它破坏了整个抽象模型 - Dog.GoForAWalk 应该返回 DogWalkReport,而不是 PossiblyNullDogWalkException。

【讨论】:

我不喜欢必须创建另一个类来报告失败的想法。所以你创建的每个可能失败的方法都需要一个报告类?似乎一个异常类很合适,如果调用者无法决定如何处理它,它可以抛出已经在子方法中设置的异常,并使用适当的堆栈跟踪。 @Maslow:Shawn 不是这么说的。 如果您的函数足够复杂 ...等等。除非在极少数情况下,否则您不应返回 Exception。【参考方案5】:

没有。

异常应该是仅在异常情况下才存在的东西(因此得名)。我唯一能看到让方法返回异常的情况是某种类型的工厂方法,其目的是专门创建异常 - 但即便如此,它也会让事情变得更糟。

如果您需要返回比布尔值更多的信息,请创建一个包含您的布尔值和其他信息的自定义类(不是从异常派生的)。

【讨论】:

仅仅因为调用者或链上更远的方法可以处理问题,并不意味着它不是一个例外,这就是 try catch 的用途,在这种情况下您不会因为接球太宽而容易受到影响,也不会因尝试接球而受到性能损失。 理由+1。例外是例外情况!如果函数返回一些东西,那意味着它成功了。如果抛出异常,则失败。 @jeffrey:那么,可以在一个方法中进行多次抛出吗? @Andy:是的。你可以在任何地方,在你需要的任意多的地方抛出异常。【参考方案6】:

绝对是对异常的非正统用法。使异常变得强大的特性是,抛出的异常可以在调用堆栈中冒泡任意数量的层。如果您只是简单地退回它,您将失去该属性(同时也会对性能产生负面影响)。

我真的不明白这与简单地返回一个包含可能错误信息的类的实例有何不同。唯一的区别是,返回从 Exception 类继承的对象可能会导致其他开发人员感到困惑,他们稍后会阅读您的代码。

因此,虽然返回包含错误信息的对象没有任何问题,但我建议为此目的使用不从 Exception 继承的类,以避免混淆。

【讨论】:

对我来说最大的不同是您将数据库/错误报告调用与嵌套/定位在同一个地方分开,而不会影响性能,也不会影响您的 catch 拖网太大。无需为每种类型创建一个特殊的类。【参考方案7】:

不,我认为这不是一个好习惯。

例外是名副其实的:它们应该是例外情况,而不是常态。以这种方式编写函数是不可能的,因为如果返回异常,则无法返回计算结果。

除非您确实拥有值得实施的恢复策略,否则您不应该在方法中处理异常。我不将记录和重新投掷视为恢复策略。

您总是可以通过抛出未经检查的异常来让您的客户不必编写 try/catch 块。

【讨论】:

我不会将这种做法用于应该返回结果的事情我使用一个通用类来保存结果或异常。再次避免了 try catch 开销。这通常用于此时的 sql 查询,但另一个主要用途是尝试提交/保存数据的方法。我不需要任何返回结果,如果成功则不需要任何返回结果,如果有问题则返回异常。 如果我想传达常见的参数错误或外部依赖错误(webservice)怎么办。有没有办法既向调用者传达各种错误消息,又让调用者不必担心错误处理?【参考方案8】:

异常在某些设置中可能很好,但请记住,它们可能会导致巨大的性能损失。

【讨论】:

我认为他的意思是抛出异常会导致性能下降。 异常并没有那么糟糕,除非你在一个紧密的循环中制造了很多异常。 异常仅在异常情况下对性能造成影响。如果您需要高性能,请避免编写中断代码:) 我没有仔细阅读。我在想抛出异常与返回异常。就性能而言,Eddie 是对的,除非您投掷(而不是返回)很多它们,否则它们并没有那么糟糕。你几乎可以做任何事情。【参考方案9】:

如果您担心其他程序员没有捕捉到它们,您可以使用代码分析工具来确保捕捉到所有异常。甚至可以编写自定义属性来实现类似 Java 中的检查异常。

话虽如此,遵循完善的约定并抛出异常不是更容易吗?如果你把它放在一个函数注释中,它应该就足够了。如果您考虑到某些特定的程序员没有捕获异常,那么您已经在处理PKBAC。

【讨论】:

【参考方案10】:

在您描述的场景中返回异常是一个坏主意,因为异常不是为此目的而设计的。

最好创建自己的包含所需信息的结构或类,然后将其返回。

public enum MyErrorType

    Abc,
    Xyz,
    Efg
;

public struct Result

    public bool Success;
    public MyErrorType ErrorType;
;

然后是方法:

private Result MyMethod()

   // code

【讨论】:

【参考方案11】:

我不得不同意,总的来说,这是一个非常糟糕的主意。

然而,我可以想到一两个可能有意义的场景。

例如,如果您正在开发一个类库项目,您可能希望抛出您自己的自定义异常,并且您可能希望在抛出这些异常之一时做一些额外的工作(例如,如果用户允许的话,电话回家) .

为了强制执行此操作,您可能希望使用工厂模式,这样只有您可以创建它们,并且您强制创建通过一个地方(您的“电话回家”代码也存在的地方)。

在这种情况下,您不希望工厂自己抛出异常,因为这会关闭您的堆栈跟踪。相反,它会将其返回到原始方法来抛出。

但像这样的情况是**cough** 例外,而不是规则。 在任何情况下我都不希望看到跨类库边界传递的异常,并且在任何情况下我都不会在不确定它是否会被抛出的情况下创建异常。

【讨论】:

【参考方案12】:

我不确定返回异常的危害,因为我这样做是为了将异常消息、类型和相关信息报告到 sql server 上的异常日志中。

某些“异常”(例如 sql server 连接失败)无法报告其他异常,我不需要再冒泡到另一个级别;我可以尝试将它们写入网络驱动器而不是崩溃。

其他类型的异常我会尽快检查调用该函数的方法是否可以处理该特定问题而不会影响实际抛出它的性能

除了这些方法,我还包含一个布尔参数,允许调用者决定是应该抛出异常还是只返回它。

【讨论】:

如果不清楚,我喜欢这样做以帮助将 ui、异常报告和一些程序逻辑分解为不同的层,而不会在适用的地方抛出性能损失。 显然在抛出异常之前调用堆栈不会被填充。我更喜欢传递异常而不是为返回失败信息的简单行为创建自定义类。【参考方案13】:

没有人这么说过:我的理解是,在 .NET 中,直到抛出异常时才会填充异常的堆栈跟踪。请参阅Best Practices for Handling Exceptions,其中写道:

堆栈跟踪从引发异常的语句开始,到捕获异常的 catch 语句结束。在决定将 throw 语句放置在何处时,请注意这一事实。

使用异常生成器方法。一个类在其实现中从不同的地方抛出相同的异常是很常见的。为避免代码过多,请使用创建异常并返回异常的辅助方法。

在 .NET 中,如果您只是实例化一个异常并返回它,那么该异常中将没有任何堆栈信息。当您使用此功能创建异常生成器方法时,这是一个好处,当您按照您询问的方式使用异常时,这是一个损失。相比之下,在 Java 中,无论是否抛出异常,都会在实例化异常时添加堆栈信息。

因此,在 .NET 中,当您返回一个从未抛出的异常时,您就放弃了告诉您问题发生在哪里的堆栈跟踪。也许你不在乎,在这种情况下,但我认为值得指出。

在任何现代 JVM 和任何最近的 .NET 版本中,抛出异常的性能开销都相当小。返回一个异常而不是抛出它——出于性能原因——是一种不恰当的优化......除非你已经分析了你的程序并且发现一个特定的抛出确实是一个瓶颈,并且返回例外会产生有意义的差异。 (这会让我感到惊讶,但一切皆有可能。)

注意:假设您创建了一个在其他情况下为 void 的方法,该方法在出错时返回异常。这意味着如果您想检查错误,您必须检查 null 以查看是否有错误。作为一般规则,在错误时返回 null 的方法会导致代码脆弱。例如,返回 List 的方法应该返回一个空 List,而不是 null。

还请注意,在 .NET 中,当您重新抛出一个异常时,因为堆栈是在 throw 子句中填充的,您通常会丢失原始堆栈跟踪。为避免这种情况,请使用裸 throw 而不指定 Exception 实例。如果您throw ex,那么您将丢失原始堆栈跟踪。

【讨论】:

这也许是最重要的一点:你还有堆栈跟踪吗?没有堆栈跟踪,几乎没有意义。 @eddie:太棒了,很棒的信息,eddie,干杯。所以,我被困住了。如何以统一的方式与方法的调用者沟通出现问题(无论是否预期)? 如果你想传达一些错误,包括堆栈跟踪,你唯一的选择是抛出一个异常。如果您只想传达成功/失败并且不关心堆栈跟踪,那么对于否则为无效的方法,返回一个布尔值;从返回值的方法中,在错误时返回一个特殊值(null、0、-1、NaN、String.empty、...) @eddie:谢谢 eddie,所以在 void 和 bool 返回的场景中,有没有办法既向调用者传达各种错误消息,又让调用者不必担心错误处理? 仅根据其他人的建议,创建一个包含您的返回结果和错误信息的复合类。如果你不想让调用者担心错误处理,只需抛出异常,让调用者的调用者担心。 :)【参考方案14】:

例外仅适用于特殊情况。把它留给导致异常情况的人(调用者)来解决问题。假设您有一个方法可以将两个数字相除

int Div(int x, int y)

return x/y;

您将如何为该方法实施适当的错误处理?在以下情况下,您会为调用返回什么值?

Div(10,0);
or(int.MinValue,-1); //result of int.MinValue == int.MaxValue + 1;

在这两种情况下没有“正确”的操作,这取决于方法的使用。 通常,在出现意外情况时中断代码执行是一个好主意。

错误处理不止try]catch。无论进入 catch 块,都需要根据错误做出明智的决定。简单地捕获异常并返回它不是错误处理;它隐藏了症状,并且最多会使调试变得非常困难并且经常是噩梦。

【讨论】:

【参考方案15】:

我经常使用这种模式返回异常以避免多个混淆代码的 trycatch 块,所以我想说,在某些情况下返回异常是个好主意。

public void Method(string text)

   //throw exception if needed
   throw new Exception("exception"); 


public bool TryMethod(string text, out Exception ex)

   try
   
      Method("test")
      ex = null;
      return true;
   
   catch(Exception e)
   
      ex = e;
      return false;
   

【讨论】:

【参考方案16】:

我同意返回而不是抛出异常是一种不好的做法,由于上述所有原因,几乎在所有情况下,除了一个函数的工作是构建异常的情况。例如,如果您使用的 Com 库只返回结果而不是引发异常,您可以创建一个将结果转换为异常的函数:

static public ErrorException ReturnErrAsExcep()

    int errcd = 0;
    string errmsg = "";

    CrummyComLib.GetLastError(out errcd, out errmsg);
    ErrorException ex = new SAPErrorException(errmsg, errcd);
    return ex;
 

我喜欢用哪个

int retval = CrummyComObject.DoStuff();

if (retval != 0)
    throw globals.ReturnErrAsExcep();

【讨论】:

以上是关于从您的方法返回异常是不好的做法的主要内容,如果未能解决你的问题,请参考以下文章

在微服务 REST api 调用中返回对象列表是不好的做法吗?

让二传手返回“this”是不好的做法吗?

从 React 钩子返回 JSX 元素是不好的做法吗? [关闭]

从 try catch finally 块中返回是不好的做法吗?

为啥让 viewController 自行关闭是不好的做法?

T-SQL 抛出异常并返回(不使用 TRY)