Windows窗体应用程序中异常处理的最佳实践?

时间:2008-10-08 16:13:44

标签: c# winforms exception-handling

我目前正在编写我的第一个Windows窗体应用程序。我现在已经阅读了一些C#书籍,所以我对C#处理异常的语言功能有了比较深入的了解。它们都非常理论化,所以我还没有想到如何在我的应用程序中将基本概念转换为一个良好的异常处理模型。

有人愿意分享关于这个主题的任何智慧珍珠吗?发布你看过像我这样的新手所犯的常见错误,以及处理异常的一般建议,使我的应用程序更加稳定和健壮。

我目前正在努力解决的主要问题是:

  • 我什么时候应该重新抛出异常?
  • 我应该尝试某种中央错误处理机制吗?
  • 与预先测试诸如磁盘上的文件之类的内容相比,处理可能引发的异常会产生性能损失吗?
  • 是否所有可执行代码都包含在try-catch-finally块中?
  • 是否有空的捕获块可以接受?

感谢所有建议!

16 个答案:

答案 0 :(得分:79)

再多几点......

您绝对应该有一个集中的异常处理策略。这可以像在try / catch中包装Main()一样简单,快速向用户发送优雅的错误消息。这是“最后的手段”异常处理程序。

如果可行,抢先检查总是正确的,但并不总是完美的。例如,在检查文件存在的代码和打开文件的下一行之间,文件可能已被删除或其他一些问题可能会妨碍您的访问。你仍然需要在那个世界中尝试/ catch / finally。根据需要同时使用抢先检查和try / catch /。

永远不要“吞下”异常,除非在最完整记录的情况下,当你绝对时,确定抛出的异常是适合居住的。这几乎不会是这种情况。 (如果确实如此,请确保您只吞下特定的异常类 - 不要永远吞下System.Exception。)

在构建库(由您的应用程序使用)时,不要吞下异常,也不要害怕异常冒泡。除非你有一些有用的东西要添加,否则不要重新投掷。不要(在C#中)这样做:

throw ex;

因为你将擦除调用堆栈。如果必须重新抛出(有时需要,例如使用企业库的异常处理块),请使用以下命令:

throw;

在一天结束时,正在运行的应用程序抛出的绝大多数异常都应该暴露在某处。它们不应该暴露给最终用户(因为它们通常包含专有或其他有价值的数据),而是通常记录,并通知管理员异常。可以向用户显示通用对话框,可能带有参考编号,以保持简单。

.NET中的异常处理更多的是艺术而非科学。每个人都会在这里分享他们的最爱。这些只是我从第1天开始使用.NET获得的一些技巧,这些技巧不止一次地保存了我的培根。您的里程可能会有所不同。

答案 1 :(得分:61)

有一个很好的代码CodeProject article here。以下是一些亮点:

  • 计划最差*
  • 提前检查
  • 不信任外部数据
  • 唯一可靠的设备是:视频,鼠标和键盘。
  • 写作也可能失败
  • 安全代码
  • 不要抛出新的异常()
  • 不要在Message字段中放置重要的异常信息
  • 每个帖子放一个catch(Exception ex)
  • 应发布通用例外
  • Log Exception.ToString();永远不要只记录Exception.Message!
  • 每个帖子不要多次捕获(异常)
  • 不要吞下例外
  • 清理代码应放在finally块
  • 到处使用“使用”
  • 不要在错误条件下返回特殊值
  • 不要使用例外来表示没有资源
  • 不要将异常处理用作从方法返回信息的方法
  • 对不应忽略的错误使用例外
  • 重新抛出异常时不要清除堆栈跟踪
  • 避免在不添加语义值的情况下更改异常
  • 例外情况应标记为[可序列化]
  • 如有疑问,请不要断言,抛出异常
  • 每个异常类至少应包含三个原始构造函数
  • 使用AppDomain.UnhandledException事件时要小心
  • 不要重新发明轮子
  • 不要使用非结构化错误处理(VB.Net)

答案 2 :(得分:15)

请注意,Windows窗体具有自己的异常处理机制。如果单击窗体中的按钮并且其处理程序抛出未在处理程序中捕获的异常,则Windows窗体将显示其自己的“未处理的异常对话框”。

要防止显示未处理的异常对话框并捕获此类异常以进行日志记录和/或提供您自己的错误对话框,您可以在Main()中调用Application.Run()之前附加到Application.ThreadException事件方法

答案 3 :(得分:14)

到目前为止,这里发布的所有建议都很好,值得注意。

我想要扩展的一件事是你的问题“处理可能抛出的异常与先发制人地测试诸如磁盘上的文件是否存在之类的东西相比会产生性能损失吗?”

天真的经验法则是“尝试/捕获块很昂贵”。事实并非如此。尝试并不昂贵。这是捕获,系统必须创建一个Exception对象并使用堆栈跟踪加载它,这是昂贵的。在很多情况下,异常足够特殊,将代码包装在try / catch块中完全没问题。

例如,如果您正在填充字典,请执行以下操作:

try
{
   dict.Add(key, value);
}
catch(KeyException)
{
}

通常比这样做更快:

if (!dict.ContainsKey(key))
{
   dict.Add(key, value);
}

对于您要添加的每个项目,因为只有在添加重复键时才会抛出异常。 (LINQ聚合查询执行此操作。)

在你给出的例子中,我几乎不假思索地使用try / catch。首先,只是因为当你检查它时文件存在并不意味着它在你打开它时会存在,所以你应该真正处理异常。

其次,我认为更重要的是,除非您的a)您的流程正在打开数千个文件,并且b)尝试打开不存在的文件的几率不是很低,创建异常的性能损失不大你会注意到的事情。一般来说,当您的程序尝试打开文件时,它只会尝试打开一个文件。在这种情况下,编写更安全的代码几乎肯定会比编写最快的代码更好。

答案 4 :(得分:9)

以下是我遵循的一些指南

  1. 快速失败:这是一个异常生成指南,对于您所做的每个假设以及您进入函数的每个参数都要进行检查,以确保您从正确的方向开始数据以及您所做的假设是正确的。典型的检查包括,参数not null,预期范围内的参数等。

  2. 当重新抛出保留堆栈跟踪时 - 这简单地转换为在重新抛出时使用throw而不是throw new Exception()。或者,如果您认为可以添加更多信息,则将原始异常包装为内部异常。但是如果你只是为了记录它而捕获异常,那么一定要使用throw;

  3. 不要捕捉你无法处理的异常,所以不要担心像OutOfMemoryException这样的事情,因为如果它们发生,你将无法做很多事情。

  4. 挂钩全局异常处理程序并确保记录尽可能多的信息。对于winforms挂钩appdomain和线程未处理的异常事件。

  5. 只有在分析代码并发现它导致性能瓶颈时才应考虑性能,默认情况下优化可读性和设计。所以关于文件存在检查的原始问题,我会说这取决于,如果你可以对文件不存在做一些事情,那么是这样检查否则如果你要做的就是抛出异常,如果文件是不是那时我没有看到这一点。

  6. 肯定有一些时候需要空的catch块,我想那些说不然的人还没有处理过多个版本的代码库。但是应该对它们进行评论和审查,以确保它们真的需要它们。最典型的例子是开发人员使用try / catch将字符串转换为整数而不是使用ParseInt()。

  7. 如果您希望代码的调用者能够处理错误条件,那么创建自定义异常,详细说明未处理的情况并提供相关信息。否则,只要尽可能坚持内置的异常类型。

答案 5 :(得分:4)

我喜欢不抓住任何我不打算处理的东西的理念,无论在我的特定环境中处理意味着什么。

当我看到如下代码时,我讨厌它:

try
{
   // some stuff is done here
}
catch
{
}

我不时地看到这一点,当有人'吃'例外时很难找到问题。我有一个同事这样做,它往往最终成为稳定问题的一个贡献者。

如果我的特定类需要做一些事情来响应异常,我会重新抛出但是问题需要冒出来然后调用它发生的方法。

我认为应该主动编写代码,异常应该用于特殊情况,而不是避免测试条件。

答案 6 :(得分:4)

我刚刚离开,但会简要介绍一下如何使用异常处理。当我回来时,我会尝试解决你的其他问题:)

  1. 明确检查所有已知错误情况*
  2. 如果您不确定是否能够处理所有情况,请在代码周围添加try / catch
  3. 如果要调用的.NET接口抛出异常
  4. ,请在代码周围添加try / catch
  5. 如果代码超过复杂性阈值
  6. ,请在代码周围添加try / catch
  7. 如果要进行健全性检查,请在代码周围添加try / catch:您声明这不应该发生
  8. 作为一般规则,我不会使用例外来代替返回码。这对.NET来说很好,但不是我。我确实对此规则有异常(呵呵),它取决于您正在处理的应用程序的体系结构。
  9. *在合理范围内。没有必要检查宇宙射线是否会击中你的数据导致一些比特被翻转。 理解什么是“合理的”是工程师获得的技能。很难量化,但很容易直觉。也就是说,我可以很容易地解释为什么我在任何特定情况下使用try / catch,但我很难用同样的知识灌输另一个。

    我倾向于避开基于异常的大型架构。 try / catch没有这样的性能命中,当抛出异常并且代码可能必须在处理它之前走几层调用堆栈时,命中就会出现。

答案 7 :(得分:4)

试图坚持的黄金法则是尽可能靠近源处理异常。

如果必须重新抛出异常,请尝试添加它,重新抛出FileNotFoundException并没有多大帮助,但抛出一个ConfigurationFileNotFoundException将允许它被捕获并在链的某个地方起作用。

我尝试遵循的另一个规则是不使用try / catch作为程序流的一种形式,所以我确实验证了文件/连接,确保对象已经启动,等等..在使用它们之前。尝试/捕获应该是例外,你无法控制的事情。

对于空的catch块,如果您在生成异常的代码中执行任何重要操作,则应至少重新抛出异常。如果没有运行异常的代码没有运行,为什么你首先编写它。

答案 8 :(得分:2)

例外情况既昂贵又必要。您不需要在try catch中包装所有内容,但确实需要确保始终捕获异常。其中很大程度上取决于您的设计。

如果让异常上升也会做同样的话,不要重新抛出。 永远不要忽视错误。

示例:

void Main()
{
  try {
    DoStuff();
  }
  catch(Exception ex) {
    LogStuff(ex.ToString());
  }

void DoStuff() {
... Stuff ...
}

如果DoStuff出错了,你还是希望它能够保释。异常将被抛到main,你会在ex。

的堆栈跟踪中看到一系列事件

答案 9 :(得分:1)

  

什么时候应该重新抛出异常?

无处不在,但最终用户方法......如按钮点击处理程序

  

我应该尝试某种中央错误处理机制吗?

我写了一个日志文件......对于WinForm应用来说非常简单

  

与预先测试诸如磁盘上的文件之类的内容相比,处理可能引发的异常是否会带来性能损失?

我不确定这一点,但我认为提示异常是一个好习惯...我的意思是你可以问一个文件是否存在以及它是否不会抛出FileNotFoundException

  

是否所有可执行代码都包含在try-catch-finally块中?

叶氏

  

是否有可能接受空挡块?

是的,假设您要显示日期,但您不知道该日期是如何存储(dd / mm / yyyy,mm / dd / yyyy等)您尝试解析它但是如果它失败只是保持如果它与你无关......我会说是的,有

答案 10 :(得分:1)

我很快学到的一件事就是将绝对每个代码块与我程序流程外的任何进行交互(即文件系统,数据库调用,用户输入)带有try-catch块。尝试捕获可能会导致性能损失,但通常在代码中的这些位置它将不会引起注意,并且它将为安全带来收益。

我在其中使用空的捕获块用户可能会做一些并非真正“不正确”的事情,但它可能会引发异常......如果用户DoubleCLicks左上方的灰色占位符单元格,它会触及一个示例,即GridView中的一个例子CellDoubleClick事件,但单元格不属于一行。在这种情况下,你确实需要发布消息,但如果你没有捕获它,它将向用户抛出未处理的异常错误。

答案 11 :(得分:1)

当重新抛出异常时,关键词会自行抛出。这将抛出捕获的异常,仍然可以使用堆栈跟踪来查看它的来源。

Try
{
int a = 10 / 0;
}
catch(exception e){
//error logging
throw;
}
执行此操作将导致堆栈跟踪在catch语句中结束。 (避免这种情况)

catch(Exception e)
// logging
throw e;
}

答案 12 :(得分:1)

根据我的经验,当我知道我将要创建它们时,我认为它适合捕捉异常。例如,当我在Web应用程序中并且我正在执行Response.Redirect时,我知道我将获得System.ThreadAbortException。因为它是故意的,我只是抓住特定的类型并且只是吞下它。

try
{
/*Doing stuff that may cause an exception*/
Response.Redirect("http:\\www.somewhereelse.com");
}
catch (ThreadAbortException tex){/*Ignore*/}
catch (Exception ex){/*HandleException*/}

答案 13 :(得分:1)

我非常同意以下规则:

  • 永远不要忽视错误。

原因是:

  • 当您第一次写下代码时,很可能您不会完全了解三方代码,.NET FCL或者您的同事最新贡献。实际上,在您完全了解每个异常可能性之前,您都不能拒绝编写代码。所以
  • 我常常发现我使用try / catch(Exception ex)只是因为我想保护自己免受未知事物的影响,并且,正如您所注意到的,我捕获了Exception,而不是更具体的例如OutOfMemoryException等。而且,我总是使异常被弹出给我(或QA) ForceAssert.AlwaysAssert(false,ex.ToString());

ForceAssert.AlwaysAssert是我个人的Trace.Assert方式,无论是否 定义了DEBUG / TRACE宏。

开发周期可能是:我注意到丑陋的Assert对话框或其他人向我抱怨它,然后我回到代码中找出引发异常的原因并决定如何处理它。

通过这种方式,我可以在短时间内写下 MY 代码并保护我免受未知领域的影响,但是如果发生异常事件总是被注意到,通过这种方式,系统变得安全且更安全

我知道你们很多人都不同意我的观点,因为开发人员应该知道他/她的代码的每一个细节,坦率地说,我在过去也是一个纯粹主义者。但是现在我了解到上述政策更加务实。

对于WinForms代码,我始终遵守的一条黄金法则是:

  • 始终尝试/捕获(例外)您的事件处理程序代码

这将保护您的UI始终可用。

对于性能损失,仅在代码达到catch时执行性能损失,执行try代码而不引发实际异常没有显着影响。

异常应该发生的几率很小,否则不是例外。

答案 14 :(得分:1)

您可以捕获ThreadException事件。

  1. 在解决方案资源管理器中选择一个Windows应用程序项目。

  2. 双击生成的Program.cs文件。

  3. 将以下代码行添加到代码文件的顶部:

    using System.Threading;
    
  4. 在Main()方法中,将以下内容添加为该方法的第一行:

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
    
  5. 在Main()方法下面添加以下内容:

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        // Do logging or whatever here
        Application.Exit();
    }
    
  6. 添加代码以处理事件处理程序中未处理的异常。 上面的代码处理了未在应用程序中其他任何地方处理的任何异常。最常见的是,此代码应记录错误并向用户显示一条消息。

引用:https://blogs.msmvps.com/deborahk/global-exception-handler-winforms/

答案 15 :(得分:-1)

你必须考虑用户。应用程序崩溃是用户想要的最后事物。 因此,任何可能失败的操作都应该在ui级别有一个try catch块。 没有必要在每个方法中使用try catch,但每次用户执行某些操作时,它必须能够处理一般异常。 这绝不会让你在第一种情况下无法检查所有内容以防止异常,但是没有没有错误的复杂应用程序,操作系统可以轻松添加意外问题,因此您必须预测意外情况并确保用户是否想要使用一个由于应用程序崩溃,操作不会导致数据丢失。 没有必要让您的应用程序崩溃,如果您捕获异常,它将永远不会处于不确定状态,并且用户总是会因崩溃而感到不便。 即使异常处于最高级别,也不会崩溃意味着用户可以快速重现异常或至少记录错误消息,因此可以帮助您解决问题。 当然不仅仅是获取一个简单的错误消息,然后只看到Windows错误对话框或类似的内容。

这就是为什么你不能只是自负,并认为你的应用没有错误,这是不能保证的。 并且这是一个非常小的努力来包装一些关于适当代码的try catch块并显示错误消息/记录错误。

作为一个用户,无论什么时候眉毛或办公室应用程序或任何崩溃,我都会感到非常生气。 如果异常太高以至于应用无法继续,那么最好显示该消息并告诉用户该做什么(重新启动,修复某些操作系统设置,报告错误等)而不是简单崩溃,那就是它。