Best Practice for Exception Handling in a Windows Forms Application?
我目前正在编写我的第一个Windows Forms应用程序。我现在已经阅读了一些C#书籍,所以我对C#处理异常的语言功能有了比较深入的了解。它们都非常理论化,所以我还没有想到如何在我的应用程序中将基本概念转换为一个良好的异常处理模型。
有人愿意分享关于这个主题的任何智慧珍珠吗?发布你看过像我这样的新手所犯的常见错误,以及处理异常的一般建议,使我的应用程序更加稳定和健壮。
我目前正在努力解决的主要问题是:
- 什么时候应该重新抛出异常?
- 我应该尝试某种中央错误处理机制吗?
- 与预先测试诸如磁盘上的文件之类的内容相比,处理可能引发的异常会产生性能损失吗?
- 是否所有可执行代码都包含在try-catch-finally块中?
- 有空的挡块是否可以接受?
感谢所有建议!
再多几点......
您绝对应该有一个集中的异常处理策略。这可以像在try / catch中包装
如果可行,抢先检查总是正确的,但并不总是完美的。例如,在检查文件存在的代码和打开文件的下一行之间,文件可能已被删除或其他一些问题可能会妨碍您的访问。你仍然需要在那个世界中尝试/ catch / finally。根据需要同时使用抢先检查和try / catch / finally。
永远不要"吞下"异常,除非在绝对正确记录的情况下,确定抛出的异常是适合居住的。这几乎不会是这种情况。 (如果确实如此,请确保您只吞下特定的异常类 - 不要吞下
在构建库(由您的应用程序使用)时,不要吞下异常,也不要害怕异常冒泡。除非你有一些有用的东西要添加,否则不要重新投掷。不要(在C#中)这样做:
1 | throw ex; |
因为你将擦除调用堆栈。如果必须重新抛出(有时需要,例如使用企业库的异常处理块时),请使用以下命令:
1 | throw; |
在一天结束时,正在运行的应用程序抛出的绝大多数异常都应该暴露在某处。它们不应该暴露给最终用户(因为它们通常包含专有或其他有价值的数据),而是通常记录,并通知管理员异常。可以向用户呈现通用对话框,可以带有参考编号,以使事情变得简单。
.NET中的异常处理更多的是艺术而非科学。每个人都会在这里分享他们的最爱。这些只是我从第1天开始使用.NET获得的一些技巧,这些技巧不止一次地保存了我的培根。你的旅费可能会改变。
这里有一篇优秀的代码CodeProject文章。以下是一些亮点:
- 计划最坏的*
- 提早检查
- 不要相信外部数据
- 唯一可靠的设备是:视频,鼠标和键盘。
- 写作也可能失败
- 安全编码
- 不要抛出新的异常()
- 不要在Message字段上放置重要的异常信息
- 每个线程放一个catch(Exception ex)
- 应发布通用异常
- Log Exception.ToString();永远不要只记录Exception.Message!
- 每个线程不要多次捕获(异常)
- 永远不要吞下异常
- 清理代码应该放在finally块中
- 到处使用"使用"
- 不要在错误条件下返回特殊值
- 不要使用异常来指示缺少资源
- 不要将异常处理用作从方法返回信息的方法
- 对不应忽略的错误使用异常
- 重新抛出异常时,请勿清除堆栈跟踪
- 避免在不添加语义值的情况下更改异常
- 例外应标记为[可序列化]
- 如有疑问,请不要断言,抛出异常
- 每个异常类至少应该有三个原始构造函数
- 使用AppDomain.UnhandledException事件时要小心
- 不要重新发明轮子
- 不要使用非结构化错误处理(VB.Net)
请注意,Windows窗体具有自己的异常处理机制。如果单击表单中的按钮并且其处理程序抛出未在处理程序中捕获的异常,则Windows窗体将显示其自己的"未处理的异常"对话框。
为了防止显示未处理的异常对话框并捕获此类异常以进行日志记录和/或提供您自己的错误对话框,您可以在Main()方法中调用Application.Run()之前附加到Application.ThreadException事件。
到目前为止,这里发布的所有建议都很好,值得注意。
我想要扩展的一件事是你的问题"与先前测试磁盘上的文件是否存在等问题相比,处理可能抛出的异常会产生性能损失吗?"
天真的经验法则是"尝试/捕获块很昂贵"。事实并非如此。尝试并不昂贵。这是捕获,系统必须创建一个Exception对象并使用堆栈跟踪加载它,这是昂贵的。在很多情况下,异常足够特殊,将代码包装在try / catch块中完全没问题。
例如,如果您正在填充字典,则:
1 2 3 4 5 6 7 | try { dict.Add(key, value); } catch(KeyException) { } |
通常比这样做更快:
1 2 3 4 | if (!dict.ContainsKey(key)) { dict.Add(key, value); } |
对于您要添加的每个项目,因为只有在添加重复键时才会抛出异常。 (LINQ聚合查询执行此操作。)
在你给出的例子中,我几乎不假思索地使用try / catch。首先,只是因为当你检查它时文件存在并不意味着它在你打开它时会存在,所以你应该真正处理异常。
其次,我认为更重要的是,除非你的a)你的流程打开了成千上万的文件,b)它试图打开不存在的文件的几率并不是很低,创建例外的性能不是你的'我会注意到的。一般来说,当您的程序尝试打开文件时,它只会尝试打开一个文件。在这种情况下,编写更安全的代码几乎肯定会比编写最快的代码更好。
以下是我遵循的一些指导原则
快速失败:这是一个异常生成指南,对于您所做的每个假设以及您进入函数的每个参数都要进行检查,以确保您开始使用正确的数据并假设您'制作是正确的。典型的检查包括,参数not null,预期范围内的参数等。
当重新抛出保留堆栈跟踪时 - 这简单地转换为在重新抛出时使用throw而不是throw new Exception()。或者,如果您认为可以添加更多信息,则将原始异常包装为内部异常。但是如果你只是为了记录它而捕获异常,那么肯定会使用throw;
不要捕获你无法处理的异常,所以不要担心像OutOfMemoryException这样的事情,因为如果它们发生,你将无法做任何事情。
挂钩全局异常处理程序并确保记录尽可能多的信息。对于winforms挂钩appdomain和线程未处理的异常事件。
只有在分析代码并发现它导致性能瓶颈时才应考虑性能,默认情况下优化可读性和设计。所以关于文件存在检查的原始问题,我会说这取决于,如果你可以对文件不存在做一些事情,那么是这样检查否则如果你要做的就是抛出异常,如果文件是不是那里我没有看到这一点。
肯定有些时候需要空的catch块,我认为那些说不然的人还没有处理过几个版本的代码库。但是应该对它们进行评论和审查,以确保它们真的需要它们。最典型的例子是开发人员使用try / catch将字符串转换为整数而不是使用ParseInt()。
如果您希望代码的调用者能够处理错误条件,那么创建自定义异常,详细说明非特定情况,并提供相关信息。否则只要尽可能坚持内置的异常类型。
试图坚持的黄金法则是尽可能靠近源处理异常。
如果你必须重新抛出一个异常尝试添加它,重新抛出一个FileNotFoundException并没有多大帮助,但抛出一个ConfigurationFileNotFoundException将允许它被捕获并在链的某个地方采取行动。
我尝试遵循的另一个规则是不使用try / catch作为程序流的一种形式,因此我确实验证了文件/连接,确保对象已经启动,等等..在使用它们之前。尝试/捕获应该是例外,你无法控制的事情。
至于一个空的catch块,如果你在生成异常的代码中做了任何重要的事情,你应该至少重新抛出异常。如果没有运行异常的代码的后果没有运行,为什么你首先编写它。
我刚刚离开,但会简要介绍一下使用异常处理的地方。当我回来时,我会尝试解决你的其他问题:)
*在合理范围内。没有必要检查宇宙射线是否会击中你的数据导致一些比特被翻转。
理解什么是"合理的"是工程师获得的技能。很难量化,但很容易直觉。也就是说,我可以很容易地解释为什么我在任何特定的实例中使用try / catch,但我很难用同样的知识灌输另一个。
我倾向于避开基于异常的大型架构。 try / catch没有这样的性能命中,当抛出异常并且代码可能必须在处理它之前走几层调用堆栈时命中。
我喜欢不抓住任何我不想处理的东西的理念,无论在我的特定环境中处理什么。
当我看到如下代码时,我讨厌它:
1 2 3 4 5 6 7 | try { // some stuff is done here } catch { } |
我不时地看到这一点,当有人'吃'例外时很难找到问题。我曾经做过这样的同事,它最终会成为稳定问题的一个贡献者。
如果我的特定类需要做一些事情来响应异常,我会重新抛出但是问题需要被冒出来然后调用它发生的方法。
我认为代码应该是主动编写的,异常应该是针对特殊情况,而不是为了避免测试条件。
例外是昂贵但必要的。您不需要在try catch中包装所有内容,但确实需要确保始终捕获异常。其中很大程度上取决于您的设计。
如果让异常上升同样可以做,也不要重新抛出。
永远不要忽视错误。
例:
1 2 3 4 5 6 7 8 9 10 11 12 | void Main() { try { DoStuff(); } catch(Exception ex) { LogStuff(ex.ToString()); } void DoStuff() { ... Stuff ... } |
如果DoStuff出错了你无论如何都要保释。异常将被抛到main,你会在ex的堆栈跟踪中看到一系列事件。
根据我的经验,当我知道我将创建它们时,我认为它适合捕捉异常。例如,当我在Web应用程序中并且我正在执行Response.Redirect时,我知道我将获得System.ThreadAbortException。因为它是有意的,我只是抓住特定类型并且只是吞下它。
1 2 3 4 5 6 7 | try { /*Doing stuff that may cause an exception*/ Response.Redirect("http:\\www.somewhereelse.com"); } catch (ThreadAbortException tex){/*Ignore*/} catch (Exception ex){/*HandleException*/} |
你可以捕获ThreadException事件。
在Solution Explorer中选择一个Windows应用程序项目。
双击打开生成的Program.cs文件。
将以下代码行添加到代码文件的顶部:
1 | using System.Threading; |
在Main()方法中,添加以下作为方法的第一行:
1 |
在Main()方法下面添加以下内容:
1 2 3 4 5 | static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { // Do logging or whatever here Application.Exit(); } |
添加代码以处理事件处理程序中的未处理异常。
在应用程序中任何其他位置都不处理的任何异常都由上面的代码处理。最常见的是,此代码应记录错误并向用户显示消息。
参考:https://blogs.msmvps.com/deborahk/global-exception-handler-winforms/
我非常同意以下规则:
- 永远不要忽视错误。
原因是:
- 当您第一次写下代码时,很可能您不会完全了解三方代码,.NET FCL或者您的同事最新贡献。实际上,在您完全了解每个异常可能性之前,您都不能拒绝编写代码。所以
-
我常常发现我使用try / catch(Exception ex)只是因为我想保护自己免受未知事物的影响,而且,正如你所注意到的,我捕获了Exception,而不是更具体的例如OutOfMemoryException等。而且,我总是做出异常被弹出给我(或QA)
ForceAssert.AlwaysAssert(false,ex.ToString());
ForceAssert.AlwaysAssert是我个人的Trace.Assert方式,无论是否
定义了DEBUG / TRACE宏。
开发周期可能是:我注意到丑陋的Assert对话框或其他人向我抱怨它,然后我回到代码并找出引发异常的原因并决定如何处理它。
通过这种方式,我可以在短时间内写下我的代码并保护我免受未知领域的影响,但是如果异常事件发生,总会被注意到,通过这种方式,系统变得安全且更安全。
我知道很多人不同意我的看法,因为开发人员应该知道他/她的代码的每一个细节,坦率地说,我在过去也是一个纯粹主义者。但是现在我了解到上述政策更加务实。
对于WinForms代码,我始终遵守的一条黄金法则是:
- 总是尝试/捕获(例外)您的事件处理程序代码
这将保护您的UI始终可用。
对于性能损失,仅在代码达到catch时执行性能损失,执行try代码而不引发实际异常没有显着影响。
应该以很少的机会发生异常,否则它不是例外。
我很快就学到的一件事就是绝对包含与我的程序流程之外的任何东西(即文件系统,数据库调用,用户输入)交互的每一块代码和try-catch块。尝试捕获可能会导致性能损失,但通常在代码中的这些位置它将不会引人注意并且它将为安全付出代价。我在用户可能做某些事情的地方使用了空的catch块真的"不正确",但它可以抛出一个异常...一个想到的例子是在GridView中如果用户DoubleCLicks左上角的灰色占位符单元格将触发CellDoubleClick事件,但是单元格没有t属于一排。在这种情况下,你真的不需要发布消息,但如果你没有捕获它,它将向用户抛出未处理的异常错误。
When should I re-throw an exception?
无处不在,但最终用户方法......就像按钮点击处理程序一样
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
Should all executable code be enclosed in try-catch-finally blocks?
叶氏
Are there any times when an empty catch block might be acceptable?
是的,假设您要显示日期,但您不知道该日期是如何存储(dd / mm / yyyy,mm / dd / yyyy等),您尝试解析它但如果失败则继续...如果它与你无关......我会说是,有
当重新抛出异常时,关键词会自行抛出。这将抛出捕获的异常,仍然可以使用堆栈跟踪来查看它的来源。
1 2 3 4 5 6 7 8 | Try { int a = 10 / 0; } catch(exception e){ //error logging throw; } |
这样做会导致堆栈跟踪在catch语句中结束。 (避免这个)
1 2 3 4 | catch(Exception e) // logging throw e; } |
你必须考虑用户。应用程序崩溃是用户想要的最后一件事。
因此,任何可能失败的操作都应该在ui级别有一个try catch块。
没有必要在每个方法中使用try catch,但每次用户执行某些操作时,它必须能够处理一般异常。
这绝不会让你在第一种情况下无法检查所有内容以防止异常,但是没有没有错误的复杂应用程序,操作系统可以轻松添加意外问题,因此您必须预测意外情况并确保用户是否想要使用一个操作不会因为应用程序崩溃而导致数据丢失。
没有必要让您的应用程序崩溃,如果您捕获异常,它将永远不会处于不确定状态,并且用户总是会因崩溃而感到不便。
即使异常处于最高级别,也不会崩溃意味着用户可以快速重现异常或至少记录错误消息,因此可以帮助您解决问题。
当然不仅仅是获取一个简单的错误消息,然后只看到Windows错误对话框或类似的东西。
这就是为什么你一定不要只是自负,并认为你的应用程序没有错误,这是不能保证的。
并且这是一个非常小的努力来包装一些关于适当代码的try catch块并显示错误消息/记录错误。
作为一个用户,每当眉毛或办公室应用程序或任何崩溃时,我当然会非常生气。
如果异常太高以至于应用程序无法继续,那么最好显示该消息并告诉用户该做什么(重新启动,修复一些操作系统设置,报告错误等),而不是简单地崩溃,就是这样。