异常处理,这样做是否可行?

28

我们正在努力制定一项政策,以正确处理应用程序中的异常。以下是我们的目标(摘要):

  • 仅处理特定的异常。
  • 仅处理您可以纠正的异常
  • 仅记录一次。

我们提出了一个解决方案,涉及一个通用的应用程序特定异常,并在代码片段中按如下方式工作:

try {
  // Do whatever
}
catch(ArgumentNullException ane)
{
  // Handle, optinally log and continue
}
catch(AppSpecificException)
{
  // Rethrow, don't log, don't do anything else
  throw;
}
catch(Exception e)
{
  // Log, encapsulate (so that it won't be logged again) and throw
  Logger.Log("Really bad thing", e.Message, e);
  throw new AppSpecificException(e)
}

所有异常都被记录并转换为AppSpecificException,以便不会再次记录。最终它将到达最后的事件处理程序,如果必须处理它。
我没有太多关于异常处理模式的经验...这是解决我们目标的好方法吗?它有什么主要缺点或大红色警告吗?
注意:其中一个缺点是在第一次捕获后,您失去了处理特定异常的能力(如果您调用调用另一个方法的方法并且第二个方法抛出异常,则无法处理它),但我发现我从未这样做过...我只处理一级深度的异常...
7个回答

61
如果你在异常第一次抛出时记录它,那么你不会记录完整的堆栈跟踪。
尽可能接近异常抛出时处理(即修复)异常。尽早收集与异常相关的上下文信息。但是允许异常向上传播到实际处理它们的位置。日志记录是最后一道防线的处理方式,因此应该发生在应用程序子系统的外层。
这将消除需要使用应用程序特定异常作为标记来防止记录本来就不应该被捕获的异常的需求。

5
当异常穿过堆栈层级时,堆栈跟踪会被添加。它是“到目前为止”的堆栈跟踪。 - John Saunders
4
这是最好的建议,我希望我能点赞两次! - Andrew Hare
2
如果你使用问题中提出的模式,我强烈怀疑你会后悔。从实际角度来说,与在应用层记录日志相比,它需要大量不必要的代码。 - Jeff Sternal
如果日志机制支持的话,我认为在异常发生时记录下来可能会有所帮助,但随着异常向上传递,日志条目应该被“替代”。否则,如果在解开堆栈时发生异常,而之前的异常还未记录日志,就没有任何方法记录有关该先前异常的任何信息。 - supercat
在.NET中不可能发生这种情况。 - John Saunders
显示剩余5条评论

6
不要记录异常并重新抛出它 - 调用者有责任处理/记录您生成的任何异常。
只捕获异常以处理它(例如记录它),或添加特定于上下文的信息。

0

解决堆栈跟踪问题的第一种选择:

class AppSpecificException : ApplicationException
{
    public string SpecificTrace { get; private set; }
    public string SpecificMessage { get; private set; }

    public AppSpecificException(string message, Exception innerException)
    {
        SpecificMessage = message;
        SpecificTrace = innerException.StackTrace;
    }

}

我不得不写一个示例来理解问题并检查堆栈跟踪问题,这是我的代码,请注意button2_click方法,最后我的文本框显示崩溃字符串和堆栈跟踪:
    private String internalValue;

    private void Operation1(String pField)
    {
        if (pField == null) throw new ArgumentNullException("pField");
        internalValue = pField;
    }

    private void Operation2(Object pField)
    {
        if (pField == null) throw new ArgumentNullException("pField");
        internalValue = Convert.ToInt32(pField).ToString();
    }

    private void Operation3(String pField)
    {
        if (pField == null) throw new ArgumentNullException("pField");
        internalValue = pField;
        Operation2(-1);
    }


    /// <exception cref="AppSpecificException"><c>AppSpecificException</c>.</exception>
    private void button1_Click(object sender, EventArgs e)
    {
        try
        {
            Operation1("One");
            Operation2("Two");
            Operation3("Three");
            MessageBox.Show(internalValue);
        }
        catch (ArgumentNullException ex)
        {
            textBoxException.Text = ex.Message + (char) 13 + (char) 10 + ex.StackTrace;
        }
        catch (AppSpecificException ex)
        {
            //textBoxException.Text = ex.Message + (char)13 + (char)10 + ex.StackTrace;
            throw;
        }
        catch (Exception ex)
        {
            textBoxException.Text = ex.Message + (char)13 + (char)10 + ex.StackTrace;                    
            throw new AppSpecificException("crash", ex);
        }

    }

    private void button2_Click(object sender, EventArgs e)
    {
        try
        {
            button1_Click(sender, e);
        }
        catch (AppSpecificException ex)
        {
            textBoxException.Text = ex.SpecificMessage + (char) 13 + (char) 10 + ex.SpecificTrace;
        }
    }

-1 有很多原因:继承自“ApplicationException”,使用(char)13+(char)10(如果这不是特定于平台的换行符呢?)使用ex.StackTrace而不是ex.ToString(),捕获“ArgumentNullException”但没有捕获“ArgumentException”,等等。真希望我几年前就看到这个了。 - John Saunders

0
这是解决异常处理问题的相当常见的方法(从更具体到不太具体)。
请记住,如果您想捕获特定问题,则拥有一个通用的ApplicationSpecific异常来捕获该应用程序/方法中发生的所有事情并不是一个好主意。最终尝试使用更具体的异常扩展它。
重新抛出异常是好的,更好的做法是声明方法抛出某些异常,并让调用者处理它们。这样,您将需要创建更少的代码,并且可以集中一些控件。

-1:实际上你不能“声明”一个方法抛出哪些异常。 - John Saunders

0

尝试避免创建新的异常并重新抛出,因为抛出异常会将堆栈跟踪设置为抛出异常的位置。只需简单地抛出即可。请参见Eric Lippert博客上的Too Much Reuse


0

我建议您更加深入地思考您想要使用哪些模式来“处理”错误。

如果您的处理模式只有记录或重新抛出,那么最终重新抛出的错误将被记录。因此,最终只是错误日志记录。如果您正在使用ASP.NET,请使用elmah,这样至少您的代码不会被try/catch-and-log样板代码覆盖。

只有很少几种方法可以“处理”错误,而不仅仅是记录错误。

重试。(注意无限循环)

等待并重试。

尝试不同但等效的技术(无法连接http?尝试连接https)。

确定缺失的条件(创建引发FolderNotFoundException的文件夹)

忽略错误--三思而后行,只有当错误实际上不是问题时才有意义,例如第三方库警告您某个条件不适用。


以上列表忽略了在许多情况下处理异常最有用的方式:仅仅处理请求操作未发生的事实,而不特别关注原因。如果用户要求打开文档,但加载尝试失败,则通知用户无法打开文档。为什么无法打开文档的详细信息可能有帮助,但并不像请求失败的事实那样重要。不幸的是,没有标准约定来区分“尝试操作失败而没有副作用”和... - supercat
“某些共享对象已经损坏,因此代码应该尽可能优雅地关闭而不使用它们。” 因此,遇到未预料到的异常情况的代码通常别无选择,只能猜测是否应该继续执行而无法处理其准备好应对的失败情况,是否应该假设CPU着火并进行突然关闭,或者是否应该采取介于两者之间的措施。 - supercat
然后发布一个答案或者给我(以及其他发布类似答案的人)投反对票,你为什么要在4年后来干扰我? - MatthewMartin

-1
一个关于异常处理的好方法是使用拦截。但是你必须验证这个模式是否适用于你的应用程序,这取决于架构:拦截需要一个容器。
原则是通过在方法上使用属性(自定义)将您的异常处理因素化到方法之外,然后使用容器初始化您的实例。容器将通过反射代理这些实例:它被称为实例拦截器。 您只需通过这些实例像往常一样调用您的方法,让拦截机制执行您之前或之后编写的工作即可。
请注意,您可以在方法中添加try catch来管理在拦截器中未受管理的特定异常。
Unity拦截:http://msdn.microsoft.com/en-us/library/dd140045.aspx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接