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

Jon*_*tus 117 c# exception-handling winforms

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

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

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

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

感谢所有建议!

Joh*_*udy 79

再多几点......

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

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

永远不要"吞下"异常,除非在绝对正确记录的情况下,确定抛出的异常是适合居住的.这几乎不会是这种情况.(如果是,请确保您只吞了特定的异常类-不要永远吞咽System.Exception.)

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

throw ex;
Run Code Online (Sandbox Code Playgroud)

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

throw;
Run Code Online (Sandbox Code Playgroud)

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

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

  • @supercat通过编写`ApplicationException`的特定子类来处理应用程序应该能够区分和处理的失败情况. (2认同)
  • 我认为这是一个糟糕的库设计,用不准确的异常类型隐藏失败模式.如果您实际意味着IOException或InvalidDataException,则不要抛出FileNotFoundException,因为应用程序需要对每种情况做出不同的响应.像StackOverflowException或OutOfMemoryException这样的东西不能合理地由应用程序处理,所以只要让它们冒泡,因为一个行为良好的应用程序无论如何都有集中的"最后手段"处理程序. (2认同)

Mic*_*cah 62

这里有一篇优秀的代码CodeProject文章.以下是一些亮点:

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

  • 你介意向我解释这一点吗?"不要使用例外来表明没有资源"我不太确定我理解其背后的原因.也只是一句话:这个答案根本没有解释"为什么".我知道这已经有5年了,但它仍然让我有点烦恼 (3认同)

小智 15

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

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


Rob*_*ney 14

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

我想要扩展的一件事是你的问题"与先前测试磁盘上的文件是否存在等问题相比,处理可能抛出的异常会产生性能损失吗?"

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

例如,如果您正在填充字典,则:

try
{
   dict.Add(key, value);
}
catch(KeyException)
{
}
Run Code Online (Sandbox Code Playgroud)

通常比这样做更快:

if (!dict.ContainsKey(key))
{
   dict.Add(key, value);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 在dict的情况下,你可以这样做:`dict [key] = value`,如果不是更快,它应该快. (3认同)

Sij*_*jin 9

以下是我遵循的一些指导原则

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

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

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

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

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

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

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


Omi*_*-RH 6

您可以捕获 ThreadException 事件。

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

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

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

    using System.Threading;
    
    Run Code Online (Sandbox Code Playgroud)
  4. 在 Main() 方法中,添加以下内容作为该方法的第一行:

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

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

参考: https: //blogs.msmvps.com/deborahk/global-exception-handler-winforms/


Ech*_*orm 2

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

如果让异常上升也能起到同样的作用,则不要重新抛出。永远不要让错误被忽视。

例子:

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

void DoStuff() {
... Stuff ...
}
Run Code Online (Sandbox Code Playgroud)

如果 DoStuff 出了问题,你无论如何都会希望它退出。异常将被抛出到 main 中,您将在 ex 的堆栈跟踪中看到一系列事件。