是否有合理的理由返回异常对象而不是抛出异常对象?


45

该问题旨在适用于任何支持异常处理的OO编程语言。我使用C#仅出于说明目的。

通常会在出现无法立即处理代码的问题时引发异常,然后将异常捕获在catch其他位置的子句中(通常是外部堆栈框架)。

问:是否有任何合法的情况不会引发并捕获异常,而只是从方法中返回然后将其作为错误对象传递出去?

之所以提出这个问题,是因为.NET 4的System.IObserver<T>.OnError方法仅表明了这一点:异常作为错误对象传递。

让我们看看另一种情况,验证。假设我遵循传统的观点,因此我在区分错误对象类型IValidationErrorValidationException用于报告意外错误的单独异常类型:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

System.Component.DataAnnotations名称空间的作用非常相似。)

这些类型可以按如下方式使用:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

现在我想知道,是否可以不简化以上内容:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

问:这两种不同方法的优缺点是什么?


如果您正在寻找两个单独的答案,则应提出两个单独的问题。合并它们只会使回答变得困难得多。
Bobson,

2
@鲍勃森:我认为这两个问题是互补的:前一个问题应该从广义上定义问题的一般背景,而后者则要求提供特定信息,使读者能够得出自己的结论。答案不必分别给出两个问题的明确答案。回答“介于两者之间”同样受欢迎。
stakx

5
我尚不清楚:从Exception派生ValidationError的原因是什么,如果您不打算抛出它呢?
pdr

@pdr:您是说扔东西AggregateException吗?好点子。
stakx 2013年

3
并不是的。我的意思是,如果您要抛出它,请使用异常。如果要返回它,请使用错误对象。如果预期的异常本质上确实是错误,则将其捕获并放入错误对象中。一个人是否允许多个错误完全无关紧要。
pdr

Answers:


31

是否有任何合法的情况不会引发并捕获异常,而只是从方法中返回然后将其作为错误对象传递出去?

如果从不抛出,那也不例外。它是derived来自Exception类的对象,尽管它没有遵循行为。在这一点上,将其称为Exception纯粹是语义,但我认为不抛出它没有任何错误。在我看来,不引发异常与内部函数捕获异常并防止其传播完全相同。

函数返回异常对象是否有效?

绝对。以下是一些合适的示例清单:

  • 异常工厂。
  • 一个上下文对象,报告是否有先前的错误作为准备使用的异常。
  • 保留先前捕获的异常的函数。
  • 创建内部类型异常的第三方API。

不会把它扔坏吗?

抛出异常有点像:“入狱。不要通过Go!” 在棋盘游戏《大富翁》中。它告诉编译器在不执行任何源代码的情况下跳过所有源代码直至捕获。它与错误,报告或停止错误无关。我喜欢将抛出异常作为函数的“超级返回”语句。它将代码的执行返回到比原始调用者更高的位置。

这里重要的是要了解异常的真正价值在于try/catch模式,而不是异常对象的实例化。异常对象只是状态消息。

在您的问题中,您似乎在混淆这两件事的用法:跳转到异常的处理程序,以及异常表示的错误状态。仅仅因为您采取了错误状态并将其包装在异常中并不意味着您就遵循了try/catch模式或其优点。


4
“如果从不抛出它,那也不例外。它是从Exception类派生的对象,但不遵循行为。” -这就是问题所在:Exception在对象的生产者或任何调用方法都不会抛出该对象的情况下,使用源自对象的对象是否合法?在什么地方只作为“状态消息”传递,而没有“超级返回”控制流?还是我严重违反了未说try/ catch(Exception)合同,以至于我永远都不要这样做,而要使用单独的非Exception类型?
stakx

10
@stakx程序员已经并将继续做其他程序员容易混淆的事情,但从技术上来说并不是错误的。这就是为什么我说这是语义。合法的答案是No您没有违反任何合同。那popular opinion就是你不应该那样做。编程中充满了示例,在这些示例中,您的源代码没有做错任何事情,但每个人都不喜欢它。应该始终编写源代码,以便清楚其意图。您是否真的通过使用异常来帮助其他程序员?如果是,则这样做。否则,如果不喜欢它,不要感到惊讶。
Reactgular

@cgTag您将内联代码块用于斜体会使我的大脑受到伤害
。– Frayt

51

当您具有分析情况的帮助方法并返回适当的异常后再由调用者抛出的异常时,返回异常而不是抛出异常在语义上是有意义的(可以将其称为“异常工厂”)。在此错误分析器函数中引发异常将意味着在分析本身中出了点问题,而返回异常则意味着已成功分析了错误的种类。

一个可能的用例是将HTTP响应代码转换为异常的函数:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

请注意,抛出异常意味着该方法使用错误或发生内部错误,而返回异常意味着该错误代码已成功识别。


这也有助于编译器理解代码流:如果某个方法永远不会正确返回(因为它会引发“所需的”异常),并且编译器对此一无所知,那么有时您可能需要多余的return语句来使编译器停止抱怨。
约阿希姆·绍尔

@JoachimSauer是的。我不止一次希望他们在C#中添加返回类型“从不”(never)-这表明函数必须通过抛出退出,将返回值放入是错误的。有时,您的代码唯一的工作就是解决异常。
罗伦·佩希特尔

2
@LorenPechtel永不返回的函数不是函数。这是终结者。调用类似的函数将清除调用堆栈。这类似于call System.Exit()。函数调用后的代码未执行。这听起来不像是功能的良好设计模式。
Reactgular

5
@MathewFoscarini考虑一个类,它具有多个可能以类似方式出错的方法。您希望转储一些状态信息作为异常的一部分,以指示异常发生时正在咀嚼的内容。由于DRY,您需要一份补充代码的副本。这样就要么调用一个进行修饰的函数,然后在您的抛出中使用结果,要么意味着该函数先构建带字的文本,然后将其抛出。我通常认为后者更好。
罗伦·佩希特尔

1
@LorenPechtel啊,是的,我也做到了。好吧,我现在明白了。
Reactgular

5

从概念上讲,如果异常对象是操作的预期结果,则为是。我能想到的情况在某些时候总是涉及到掷球接球:

  • “ Try”模式的变体(一种方法封装了第二种引发异常的方法,但是捕获了异常,而是返回一个指示成功的布尔值)。您可以返回一个抛出的异常(如果成功,则返回null),而不是返回一个布尔值,从而允许最终用户获得更多信息,同时保留无捕获的成功或失败指示。

  • 工作流错误处理。在实践中,与“尝试模式”方法封装相似,您可能在命令链模式中抽象了工作流步骤。如果链中的某个链接引发异常,则通常更干净地在抽象对象中捕获该异常,然后通过上述操作将其返回,因此工作流引擎和作为工作流步骤运行的实际代码不需要大量尝试-捕获自己的逻辑。


我认为没有人能在“尝试”模式上倡导这种变化,但是返回布尔值的“推荐”方法似乎很贫乏。我认为最好有一个抽象OperationResult类,该类具有bool“成功”属性,一个GetExceptionIfAny方法和一个AssertSuccessful方法(GetExceptionIfAny如果非null则抛出异常)。在GetExceptionIfAny没有太大用处的情况下,使用方法而不是属性可以使用静态不可变错误对象。
2014年

5

可以说您将某些任务排队在某个线程池中。如果此任务引发异常,则它是不同的线程,因此您不会捕获它。执行它的线程就死了。

现在,考虑某种(该任务中的代码或线程池实现)捕获该异常并将其与任务一起存储,并认为任务已完成(未成功)。现在,您可以询问任务是否完成(并询问它是否已抛出,或者可以再次在您的线程中抛出(或者更好的是,将新异常以原始原因作为原因))。

如果您手动执行此操作,您会注意到您正在创建新的异常,将其抛出并捕获,然后将其存储在另一个线程中,以检索对其进行抛出,捕获和反应。因此,跳过投掷和捕获并仅将其存储并完成任务,然后仅询问是否存在异常并对它做出反应是有意义的。但这会导致更复杂的代码,如果在同一位置可能确实抛出异常。

PS:这是使用Java编写的,在Java中创建异常时创建堆栈跟踪信息(与在抛出时在C#中创建堆栈跟踪信息不同)。因此,Java中没有抛出异常的方法将比C#中的慢(除非它是预先创建和重用的),但是将提供堆栈跟踪信息。

通常,我会远离创建异常并从不抛出异常(除非性能分析后的性能优化指出了瓶颈)。至少在Java中,异常创建非常昂贵(堆栈跟踪)。在C#中是可能的,但是IMO令人惊讶,因此应避免。


错误侦听器对象也经常发生这种情况。队列将执行任务,捕获任何异常,然后将该异常传递给负责的错误侦听器。在这种情况下,杀死对工作不太了解的任务线程通常没有意义。这种想法也被内置到的Java API,其中一个线程可以从另一个传递例外: docs.oracle.com/javase/1.5.0/docs/api/java/lang/...
兰斯Nanek

1

这取决于您的设计,通常我不会将异常返回给调用者,而是会抛出并捕获它们并将其留在那。通常,编写代码会导致早期失败。例如,考虑打开文件并对其进行处理的情况(这是C#PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

在这种情况下,一旦遇到不良记录,我们将出错并停止处理该文件。

但是请说,要求是即使一行或多行出现错误,我们也要继续处理文件。代码可能看起来像这样:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

因此,在这种情况下,我们将异常返回给调用者,尽管我可能不会将异常返回,而是将某种错误对象返回,因为异常已被捕获并已经处理。这对返回异常没有害处,但是其他调用者可以重新抛出异常,这可能是不希望的,因为异常对象具有该行为。如果希望呼叫者具有重新抛出然后返回异常的能力,否则,请从异常对象中取出信息并构造一个较小的轻量对象,然后返回该对象。快速故障通常是更干净的代码。

对于您的验证示例,由于验证错误可能很常见,因此我可能不会继承异常类或引发异常。如果50%的用户在第一次尝试时都无法正确填写表单,则返回对象而不是引发异常的开销会更少。


1

问:是否有任何合法的情况不会引发并捕获异常,而只是从方法中返回然后将其作为错误对象传递出去?

是。例如,最近我遇到一种情况,发现ASMX Web服务中引发的异常在结果SOAP消息中不包含元素,因此我必须生成它。

说明性代码:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

在大多数语言(afaik)中,异常附带了一些附加的功能。通常,当前堆栈跟踪的快照存储在对象中。这对于调试非常有用,但也可能涉及内存。

正如@ThinkingMedia已经说过的,您实际上是在使用异常作为错误对象。

在您的代码示例中,似乎主要是为了重用代码和避免编写代码。我个人认为这不是这样做的好理由。

另一个可能的原因是欺骗该语言,以便为您提供带有堆栈跟踪的错误对象。这为错误处理代码提供了更多的上下文信息。

另一方面,我们可以假设保持堆栈跟踪在周围会消耗内存。例如,如果您开始在某个地方收集这些对象,可能会看到不愉快的内存影响。当然,这很大程度上取决于如何在相应的语言/引擎中实现异常堆栈跟踪。

那么,这是个好主意吗?

到目前为止,我还没有看到它被使用或推荐。但这并不意味着什么。

问题是:堆栈跟踪对您的错误处理真的有用吗?或更笼统地说,您想向开发人员或管理员提供哪些信息?

典型的throw / try / catch模式使得有必要进行堆栈跟踪,因为否则您将不知道它来自何处。对于返回的对象,它总是来自被调用的函数。堆栈跟踪可能包含开发人员需要的所有信息,但是可能不那么沉重,更具体。


-4

答案是肯定的。请参阅http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions,并打开有关例外情况的Google C ++样式指南的箭头。您可以在此处看到他们以前决定反对的论点,但是说如果他们不得不再次做下去,他们可能会决定。

但是,值得注意的是,出于类似的原因,Go语言也不会惯用异常。


1
抱歉,但是我不明白这些准则如何帮助回答这个问题:他们只是建议完全避免异常处理。这不是我要问的;我的问题是,在永远不会抛出结果异常对象的情况下,使用异常类型描述错误条件是否合法。这些准则中似乎没有任何内容。
stakx
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.