Best Practice for Exception Handling in a Windows Forms Application?

I'm currently in the process of writing my first Windows Forms application. I've read a few C# books now so I've got a relatively good understanding of what language features C# has to deal with exceptions. They're all quite theoretical however so what I haven't got yet is a feel for how to translate the basic concepts into a good exception-handling model in my application.

Would anyone like to share any pearls of wisdom on the subject? Post any common mistakes you've seen newbies like myself make, and any general advice on handling exceptions in a way that will my application more stable and robust.

The main things I'm currently trying to work out are:

  • When should I re-throw an exception?
  • Should I try to have a central error-handling mechanism of some kind?
  • Do handling exceptions which might be thrown have a performance hit compared with pre-emptively testing things like whether a file on disk exists?
  • Should all executable code be enclosed in try-catch-finally blocks?
  • Are there any times when an empty catch block might be acceptable?

All advice gratefully received!

74482 次浏览

例外情况代价高昂,但却是必要的。您不需要将所有内容都包装在 try catch 中,但是您确实需要确保异常最终总是被捕获。这在很大程度上取决于你的设计。

如果允许异常增加也同样适用,则不要重新抛出。 不要让错误不被注意地溜走。

例如:

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


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

If DoStuff goes wrong you'll want it to bail anyway. The exception will get thrown up to main and you'll see the train of events in the stack trace of ex.

我喜欢这样的哲学: 不要抓住任何我不打算处理的东西,不管在我的特定情况下处理意味着什么。

我讨厌看到这样的代码:

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

I have seen this from time to time and it is quite difficult to find problems when someone 'eats' the exceptions. A coworker I had does this and it tends to end up being a contributor to a steady stream of issues.

如果我的特定类需要对异常执行某些操作,但是问题需要冒泡显示为发生异常的方法,则重新抛出。

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

我正要离开,但是我会简要介绍在哪里使用异常处理。当我回来的时候,我会试着解决你的其他问题:)

  1. 显式检查所有已知的错误条件 *
  2. 如果您不确定是否能够处理所有情况,请在代码周围添加 try/catch
  3. Add a try/catch around the code if the .NET interface you are calling throws an exception
  4. 如果代码超过了复杂性阈值,则在代码周围添加 try/catch
  5. 在代码周围添加一个 try/catch,如果是为了完整性检查: 您断言这永远不应该发生
  6. 作为一般规则,我不使用异常代替返回代码。这是罚款。但不是我。不过,我对这个规则也有例外(呵呵) ,这也取决于您正在开发的应用程序的体系结构。

合情合理 * 。没有必要检查,看看是否说一个宇宙射线击中你的数据导致一对夫妇位被翻转。 Understanding what is "reasonable" is an acquired skill for an engineer. It's hard to quantify, yet easy to intuit. That is, I can easily explain why I use a try/catch in any particular instance, yet I am hard pressed to imbue another with this same knowledge.

我个人倾向于避开严重基于异常的体系结构。Try/catch 本身并没有对性能造成影响,当抛出异常时,性能会受到影响,代码可能必须在调用堆栈的几个级别上行才能处理异常。

什么时候应该重新引发异常?

Everywhere, but end user methods... like button click handlers

Should I try to have a central error-handling mechanism of some kind?

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

Do handling exceptions which might be thrown have a performance hit compared with pre-emptively testing things like whether a file on disk exists?

我不确定这一点,但我相信这是一个很好的实践,如何异常... 我的意思是,你可以问一个文件是否存在,如果它没有抛出一个 FileNotFoundException

所有可执行代码都应该包含在 try-catch-finally 块中吗?

是的

空的 catch 块是否可以接受?

是的,让我们假设你想显示一个日期,但是你不知道这个日期是如何存储的(dd/mm/yyyy,mm/dd/yyyy,等等)你尝试 tp 解析它,但是如果它失败了就继续... 如果它与你无关... 我会说是的,有

我很快就学到了一件事,那就是将与 什么都行交互的 absolutely every代码块封装在我的程序流(即文件系统、数据库调用、用户输入)之外,并使用 try-catch 块。Try-catch 可能会导致性能下降,但通常在代码中的这些地方不会引起注意,而且它会为自己的安全性付出代价。我曾经在用户可能做一些不是真正“不正确”的事情的地方使用过空的 catch-block,但是它会抛出一个异常... ... 我想到的一个例子是在 GridView 中,如果用户 DoubleClick 点击左上角的灰色占位符单元格,它会触发 CellDoubleClick 事件,但是单元格不属于一行。在这种情况下,您的 不要确实需要发布消息,但是如果您没有捕捉到它,它将向用户抛出一个未处理的异常错误。

有一个非常好的代码 CodeProject 文章,这里有一些亮点:

  • 做最坏的打算 *
  • 早点检查
  • 不要信任外部数据
  • 唯一可靠的设备是: 视频、鼠标和键盘。
  • 写作也可能失败
  • 安全密码
  • 不要引发新的 Exception ()
  • 不要在 Message 字段中放置重要的异常信息
  • 每个线程放置一个捕获(例外)
  • 应该发布捕获的泛型异常
  • 记录 Exception.ToString () ; 永远不要只记录 Exception.Message!
  • 每个线程捕获(异常)不超过一次
  • 千万不要接受例外
  • Cleanup code should be put in finally blocks
  • Use "using" everywhere
  • 不要在错误条件下返回特殊值
  • 不要使用异常来表示资源不存在
  • 不要使用异常处理作为从方法返回信息的方法
  • 对不应忽略的错误使用异常
  • 重新引发异常时不要清除堆栈跟踪
  • 避免在不添加语义值的情况下更改异常
  • 异常应标记为[可序列化]
  • 当有疑问时,不要断言,抛出异常
  • 每个异常类应该至少有三个原始构造函数
  • 使用 AppDomain.UnhandledException 事件时要小心
  • 不要重新发明轮子
  • 不要使用非结构化错误处理(VB.Net)

A few more bits ...

You absolutely should have a centralized exception handling policy in place. This can be as simple as wrapping Main() in a try/catch, failing fast with a graceful error message to the user. This is the "last resort" exception handler.

如果可行,先发制人的检查总是正确的,但并不总是完美的。例如,在检查文件是否存在的代码和打开文件的下一行之间,文件可能已经被删除,或者其他问题可能会妨碍访问。在那个世界里,你仍然需要尝试/抓住/最终。适当地使用抢先检查和 try/catch/finally。

永远不要“吞下”一个异常,除非在大多数文档记录良好的情况下,当您绝对、肯定地确信被抛出的异常是可居住的。几乎不会出现这种情况。(如果是这样,请确保只吞下 具体点异常类——不要将 永远不会吞下 System.Exception。)

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

throw ex;

因为您将删除调用堆栈。如果必须重新抛出(有时是必要的,例如在使用企业库的异常处理块时) ,请使用以下方法:

throw;

最后,运行中的应用程序抛出的绝大多数异常都应该在某个地方公开。它们不应该暴露给最终用户(因为它们通常包含专有数据或其他有价值的数据) ,而应该通常进行日志记录,并通知管理员异常。用户可以看到一个通用的对话框,可能还有一个参考编号,以保持事情的简单性。

异常处理中。NET 与其说是科学,不如说是艺术。每个人都会有自己喜欢的东西在这里分享。这些只是我学到的一些技巧。NET,这些技术在不止一次的情况下拯救了我。你的情况可能会有所不同。

以下是我遵循的一些准则

  1. Fail-Fast: 这更像是一个生成异常的指导原则,对于你做出的每一个假设,以及你进入函数的每一个参数,都要检查一下,以确保你从正确的数据开始,并且你做出的假设是正确的。典型的检查包括,参数不为空,参数在预期范围内等。

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

  3. 不要捕获您无法处理的异常,所以不要担心 OutOfMemory 异常之类的事情,因为如果它们发生,您无论如何都不能执行太多操作。

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

  5. 只有当您分析了代码并发现它导致了性能瓶颈时,才应该考虑性能问题,默认情况下应该对可读性和设计进行优化。所以关于你最初关于文件存在检查的问题,我想说这取决于,如果你能做一些事情来处理文件不在那里,那么是的,做那个检查,否则如果你要做的只是抛出一个异常,如果文件不在那里,那么我看不出有什么意义。

  6. 有时候确实需要使用空的 catch 块,我认为那些不这么认为的人并没有在经过几个版本演变的代码库上工作。但是他们应该被评论和审查,以确保他们是真正需要的。最典型的例子是开发人员使用 try/catch 将字符串转换为整数,而不是使用 ParseInt ()。

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

当重新引发异常时,关键字将由其自身引发。这将抛出捕获的异常,并且仍然能够使用堆栈跟踪查看异常的来源。

Try
{
int a = 10 / 0;
}
catch(exception e){
//error logging
throw;
}

这样做将导致堆栈跟踪以 catch 语句结束

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

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

如果必须重新抛出异常,请尝试向其中添加异常,重新抛出 FileNotFoundException 没有多大帮助,但是抛出 ConfigurationFileNotFoundException 将允许捕获该异常并在链上的某个位置对其进行操作。

我尝试遵循的另一条规则是不要将 try/catch 作为程序流的一种形式使用,所以我会验证文件/连接,确保对象已经启动,等等。在使用它们之前。Try/catch 应该用于异常,即您无法控制的事情。

对于空 catch 块,如果在生成异常的代码中执行任何重要操作,则应该至少重新引发异常。如果抛出异常的代码没有产生任何后果,那么为什么一开始要编写它呢。

到目前为止,这里提供的所有建议都是好的,值得注意。

我想扩展一下您的问题: “与先发制人地测试磁盘上是否存在文件这样的事情相比,处理可能抛出的异常是否会对性能造成影响?”

幼稚的经验法则是“ try/catch 块是昂贵的”其实不是这样的。尝试并不昂贵。这就是捕获,系统必须创建一个 Exception 对象并用堆栈跟踪加载它,这是非常昂贵的。在许多情况下,异常足够异常,因此可以将代码封装在 try/catch 块中。

例如,如果你填充一个字典,这样:

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

往往比这样做更快:

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

因为只有在添加重复的键时才会引发异常。(LINQ 聚合查询执行此操作。)

在你给出的例子中,我几乎不假思索地使用 try/catch。首先,仅仅因为检查文件时该文件存在并不意味着打开它时该文件将存在,所以无论如何都应该处理异常。

其次,我认为更重要的是,除非你的 a)你的进程正在打开成千上万的文件,b)它试图打开一个不存在的文件的几率不是微不足道的低,创建异常的性能打击是你永远不会注意到的。一般来说,当程序试图打开一个文件时,它只是试图打开一个文件。在这种情况下,编写更安全的代码几乎肯定比编写尽可能快的代码要好。

根据我的经验,当我知道要创建异常时,我认为应该抓住它们。例如,当我在一个 Web 应用程序中,我正在做一个响应。重定向,我知道我会得到一个系统。异常。因为它是故意的,我只是有一个特定类型的捕获,只是吞下它。

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

我非常同意以下规则:

  • 不要让错误不被注意地溜走。

The reason is that:

  • 当您第一次写下代码时,很可能您并不完全了解三方代码。NET FCL 库,或您的同事的最新贡献。在现实中,您不能拒绝编写代码,直到您很好地了解每一种可能的异常。那么
  • 我经常发现我使用 try/catch (Exception ex)仅仅是因为我想保护自己不受未知事物的影响,而且,正如你所注意到的,我捕捉到的是 Exception,而不是更具体的例如 OutOfMemory 异常等。而且,我总是让异常弹出给我(或 QA) AlwaysAssert (false,ex.ToString ()) ;

始终坚持是我个人的跟踪方式 定义了 DEBUG/TRACE 宏。

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

通过这种方式,我可以在很短的时间内写下 我的代码,并保护我免受未知域的攻击,但是如果发生异常情况,系统总是会被注意到,这样系统就变得更加安全。

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

对于 WinForms 代码,我一直遵守的一条金科玉律是:

  • 始终尝试/catch (Exception)事件处理程序代码

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

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

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

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

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

你必须考虑用户。应用程序崩溃是用户想要的 最后事物。 因此,任何可能失败的操作都应该在 ui 级别有一个 try catch 块。 没有必要在每个方法中都使用 try catch,但是每次用户执行某些操作时,它必须能够处理通用异常。 这并不意味着你可以检查所有的东西来防止第一种情况下的异常,但是没有一个复杂的应用程序是没有错误的,操作系统可以很容易地添加意想不到的问题,因此你必须预料到意想不到的情况,并确保如果用户想要使用一个操作,不会因为应用程序崩溃而导致数据丢失。 没有必要让你的应用程序崩溃,如果你捕捉到异常,它将永远不会处于不确定的状态,用户总是因为崩溃而感到不便。 Even if the exception is at the top most level, not crashing means the user can quickly reproduce the exception or at least record the error message and therefore greatly help you to fix the problem. 当然比得到一个简单的错误消息,然后只看到窗口错误对话框或类似的东西多得多。

这就是为什么你不能只是自负,认为你的应用程序没有错误,这是不能保证的。 非常做了一些小工作,将一些 try catch 块包装到适当的代码中,并显示错误消息/记录错误。

作为一个用户,每当眉毛或者办公应用程序或者其他任何东西崩溃的时候,我肯定会非常生气。 If the exception is so high that the app can't continue it's better to display that message and tell the user what to do (restart, fix some os settings, report the bug, etc.) than to simply crash and that's it.

可以捕获 ThreadException 事件。

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

  2. 通过双击生成的 Program.cs 文件来打开它。

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

    using System.Threading;
    
  4. In the Main() method, add the following as the first line of the method:

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
    
  5. Add the following below the Main() method:

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
    // Do logging or whatever here
    Application.Exit();
    }
    
  6. Add code to handle the unhandled exception within the event handler. Any exception that is not handled anywhere else in the application is handled by the above code. Most commonly, this code should log the error and display a message to the user.

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