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


118

我目前正在编写第一个Windows Forms应用程序。我现在已经读了几本C#书,因此我对C#必须处理哪些语言功能进行了相对较好的理解。它们都是很理论的,因此我还没有想到如何在应用程序中将基本概念转换为良好的异常处理模型。

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

我目前正在尝试解决的主要问题是:

  • 我什么时候应该重新抛出异常?
  • 我是否应该尝试使用某种集中式错误处理机制?
  • 与抢先测试磁盘上是否存在文件之类的事物相比,处理可能引发的异常是否会对性能产生影响?
  • 是否应将所有可执行代码包含在try-catch-finally块中?
  • 在任何时候都可以接受一个空的catch块吗?

感谢所有意见!

Answers:


79

还有一些...

您绝对应该有一个集中的异常处理策略。这可以像包装Main()在try / catch中一样简单,失败时会向用户显示优美的错误消息。这是“万不得已”的异常处理程序。

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

绝对不要“吞噬”异常,除非在有充分记录的情况下,您绝对可以肯定地抛出异常是可以居住的。几乎永远不会这样。(如果是,请确保您只吞了特定的异常类-不要永远吞咽System.Exception。)

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

throw ex;

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

throw;

归根结底,正在运行的应用程序引发的绝大多数异常应该暴露在某个地方。它们不应暴露给最终用户(因为它们通常包含专有或其他有价值的数据),而通常应进行记录,并向管理员通知该异常。可以向用户显示一个通用对话框,也许带有一个参考数字,以使事情变得简单。

.NET中的异常处理比科学还重要。每个人都会在这里分享自己的最爱。这些只是我从第一天开始使用.NET所掌握的一些技巧,这些技巧已多次节省了我的培根。你的旅费可能会改变。


1
如果让异常冒出来,那么调用者应该如何知道异常是否表示操作失败,但是系统从根本上来说还可以(例如,用户试图打开损坏的文档文件;该文件无法加载,但是一切正常否则应该没问题),或者它是否表明CPU着火了,应该尽快驶向出口?可能会抛出ArgumentException之类的东西,具体取决于抛出它的情况。
超级猫

2
@supercat通过ApplicationException为应用程序应该合理地能够区分和处理的那些失败情况编写特定的子类。
Matt Enright

@Matt Enright:当然有可能捕获到自己的异常,但是我没有意识到任何与公约类似的东西,根据该惯例,抛出异常的模块表明它们是否指示了超出故障所隐含含义之外的任何状态的损坏。理想情况下,诸如SuperDocument.CreateFromFile()之类的方法将成功,抛出CleanFailure异常或引发SomethingReallyBadHappenedException。不幸的是,除非有人将可能引发异常的所有内容包装在自己的catch块中,否则无法知道InvalidOperationException ...
supercat

@Matt Enright:...应包装在CleanFailureException或SomethingReallyBadHappenedException中。并且将所有内容包装在单独的try-catch块中将首先破坏了具有异常的整个目的。
超级猫

2
我认为这是一个糟糕的库设计,无法使用错误类型的隐藏错误模式。如果您的实际意思是IOException或InvalidDataException,请不要抛出FileNotFoundException,因为应用程序需要对每种情况做出不同的响应。应用程序无法合理地处理诸如StackOverflowException或OutOfMemoryException之类的问题,因此请让它们冒泡,因为行为良好的应用程序无论如何都具有集中的“最后处理”处理程序。
马特·恩赖特

63

这里有一篇很棒的代码CodeProject文章。以下是一些要点:

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

3
您介意向我解释这一点吗?“不要使用异常来表示资源的缺乏”我不确定我是否理解其背后的原因。也只是一句话:这个答案根本没有解释“为什么”。我知道这是5岁,但它仍然错误我一点
雷米

引用文章的链接已过期。Code Project建议该文章可能已移至此处
DavidRR

15

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

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


感谢这个建议,我来得太晚了,我刚刚在linqpad中验证了它,按预期工作,委托是:void Form1_UIThreadException(object sender,ThreadExceptionEventArgs t)关于此主题的另一个很好的资源是 richnewman.wordpress.com/2007/ 04/08 /… 对于整体未处理的异常处理:AppDomain.UnhandledException事件用于处理从非主UI线程引发的未处理的异常。
zhaorufei 2015年

14

到目前为止,此处发布的所有建议都是不错的,值得我们注意。

我要扩展的一件事是您的问题“与先发制人的测试(例如磁盘上的文件是否存在)相比,处理可能引发的异常是否会对性能产生影响?”

幼稚的经验法则是“尝试/捕获块很昂贵”。这不是真的。尝试并不昂贵。吸引人的是,系统必须创建一个Exception对象并使用堆栈跟踪将其加载,这很昂贵。在很多情况下,异常是非常例外的,足以将代码包装在try / catch块中。

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

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

通常比这样做更快:

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

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

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

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


3
在dict的情况下,您可以执行以下操作:dict[key] = value如果不是更快,则应该一样快
。.– nawfal

9

这是我遵循的一些准则

  1. 失败快速:这更多是一个异常生成准则,对于您做出的每个假设以及您进入函数的每个参数,都要进行检查,以确保您从正确的数据开始,并且假设您做的是正确的。典型的检查包括参数不为空,参数在预期范围内等。

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

  3. 不要捕获无法处理的异常,因此不必担心OutOfMemoryException之类的事情,因为如果发生此类异常,您将无能为力。

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

  5. 仅当您分析了代码并看到它引起性能瓶颈时,才应考虑性能,默认情况下会针对可读性和设计进行优化。因此,关于您的文件存在性检查的原始问题,我想说这取决于,如果您可以对不存在的文件做点什么,那么请进行该检查,否则,如果您要做的一切只是在文件存在的情况下抛出异常不在那里,那么我看不到重点。

  6. 肯定有很多时候需要空的catch块,我认为那些不这样说的人还不能使用在多个发行版中发展了的代码库。但是应该对它们进行评论和审查,以确保确实需要它们。最典型的示例是开发人员使用try / catch将字符串转换为整数而不是使用ParseInt()。

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


钩挂appdomain和线程未处理的异常处理程序有什么用?如果仅使用Appdomain.UnhandledException,那不是可以捕获所有内容的最通用的应用程序吗?
Quagmire

Application.ThreadException当您引用“线程未处理的异常”事件时,是否意味着要处理该事件?
杰夫·B

4

我喜欢这样一种理念,即在我的特定情况下,不要捕获我不打算处理的任何内容。

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

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

我经常看到这种情况,当有人“吃掉”异常时很难发现问题。我曾经做过的一个同事这样做,并且最终导致了源源不断的问题。

如果我的特定类需要对异常做出响应,但我需要重新抛出该问题,但该问题需要冒出来解决,但无论发生在何处都称为方法。

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


@Kiquenet对不起,我不清楚。我的意思是:不要做空的捕获,而是向Dispatcher.UnhandledException添加一个处理程序,并至少添加一个日志,这样它就不会被面包屑吃掉:)但是,除非您需要一个静默组件,否则您应该总是包含异常进入您的设计。在需要抛出它们时将它们抛出,并为您明确接口/ API /任何类的合同。
LuckyLikey

4

我正要走出去,但会简要介绍一下在哪里使用异常处理。当我返回时,我将尝试解决您的其他问题:)

  1. 明确检查所有已知的错误情况*
  2. 如果不确定是否能够处理所有情况,请在代码周围添加try / catch
  3. 如果您正在调用的.NET接口引发异常,请在代码周围添加try / catch
  4. 如果代码超过您的复杂性阈值,请在代码周围添加try / catch
  5. 如果需要进行健全性检查,请在代码周围添加try / catch:您断言此事件永远不会发生
  6. 通常,我不使用异常代替返回码。.NET很好,但是对我来说不是。我确实对此规则有例外(嘿),这取决于您正在处理的应用程序的体系结构。

*在合理范围内。无需检查是否有宇宙射线击中您的数据,从而导致几比特被翻转。理解什么是“合理的”是工程师必不可少的技能。很难量化,但很容易理解。也就是说,我可以很容易地解释为什么我在任何特定情况下都使用try / catch,但是我很难再用同样的知识灌输另一个。

我倾向于避开大量基于异常的体系结构。try / catch没有这样的性能影响,当抛出异常时,这种影响就会出现,并且代码可能必须在调用堆栈之前遍历多个级别,然后才能进行处理。


4

一直坚持的黄金法则是尽可能在源头附近处理异常。

如果必须重新抛出异常,请尝试添加它,重新抛出FileNotFoundException并没有多大帮助,但是抛出ConfigurationFileNotFoundException将允许对其进行捕获并采取措施。

我尝试遵循的另一个规则是不要将try / catch用作程序流的一种形式,因此在使用文件之前,我会验证文件/连接,确保对象已启动等。尝试/捕获应该针对异常,您无法控制的事情。

至于空的catch块,如果您在生成异常的代码中做任何重要的事情,则应至少重新抛出该异常。如果代码没有引发异常的后果,那么为什么要首先编写它。


这种“黄金法则”充其量是任意的。
伊万·哈珀

3

您可以捕获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/


@Kiquenet对不起,我不知道VB
Omid-RH

2

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

如果让异常上升也一样,请不要重新抛出异常。永远不要让错误被忽略。

例:

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

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

如果DoStuff出了问题,您还是要保释。异常将被抛出给main,您将在ex的堆栈跟踪中看到一系列事件。


1

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

到处都是,但是最终用户方法...就像按钮单击处理程序一样

我是否应该尝试使用某种集中式错误处理机制?

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

与抢先测试磁盘上是否存在文件之类的事物相比,处理可能引发的异常是否会对性能产生影响?

我不确定,但是我认为抛出异常是一个好习惯……我的意思是你可以问一个文件是否存在以及是否不抛出FileNotFoundException。

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

是的

在任何时候都可以接受一个空的catch块吗?

是的,假设您想显示一个日期,但是您不知道该日期是如何存储的(dd / mm / yyyy,mm / dd / yyyy等),请尝试通过tp解析它,但是如果失败,则继续操作。如果与您无关...我会说是的


1

我很快学到的一件事是,用try-catch块将与我的程序流程之外的任何内容(例如,文件系统,数据库调用,用户输入)进行交互的每个代码块都完全封闭起来。尝试捕获可能会导致性能下降,但通常在代码的这些位置不会引起注意,并且可以安全地收回成本。

我在用户可能会做一些并非真正“不正确”的事情的地方使用了空的catch-blocks,但是它可能会引发异常...一个想到的例子是,如果用户DoubleCLicks灰色占位符,则在GridView中单元格将在左上角触发CellDoubleClick事件,但该单元格不属于一行。在这种情况下,你 确实需要发布一条消息,但是如果您不捕获该消息,则会向用户抛出未处理的异常错误。


1

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

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

这样做将导致堆栈跟踪在catch语句中结束。(避免这个)

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

这仍然适用于最新版本的.NET Framework和.NET Core吗?
LuckyLikey

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*/}

1

我深信以下规则:

  • 永远不要让错误被忽略。

原因是:

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

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

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

通过这种方式,我可以在短时间内写下我的代码,并保护我免受未知域的侵害,但是始终可以注意到异常情况是否发生,从而使系统变得越来越安全。

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

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

  • 始终尝试/捕获(异常)事件处理程序代码

这样可以保护您的用户界面始终可用。

对于性能下降而言,性能下降仅在代码达到catch时发生,在不引发实际异常的情况下执行try代码不会产生重大影响。

异常应该很少发生,否则不是异常。


-1

您必须考虑用户。应用程序崩溃是最后一次用户想要的东西。因此,任何可能失败的操作都应在ui级别具有try catch块。不必在每种方法中都使用try catch,但是每次用户执行某项操作时,它都必须能够处理通用异常。在第一种情况下,这绝对不能使您从检查所有内容中避免异常的发生,但是没有复杂的应用程序没有错误,操作系统很容易添加意外的问题,因此您必须预料到意外的情况并确保用户是否要使用意外的情况。操作不会因为应用程序崩溃而导致数据丢失。无需让您的应用程序崩溃,如果您捕获异常,它将永远不会处于不确定状态,并且用户总是因崩溃而感到不便。即使是最顶层的例外,不会崩溃意味着用户可以快速重现异常或至少记录错误消息,从而极大地帮助您解决问题。当然,不仅获得简单的错误消息,然后仅看到Windows错误对话框之类的东西,还远远不止这些。

这就是为什么您永远不要自负,并认为您的应用程序没有错误,这是不能保证的。并且包装一些有关适当代码的try catch块并显示错误消息/记录错误是非常小的工作。

作为用户,每当浏览器或Office应用程序崩溃或任何崩溃时,我肯定会非常生气。如果异常异常严重,导致应用程序无法继续运行,则最好是显示该消息并告诉用户该怎么做(重新启动,修复某些操作系统设置,报告错误等),而不是简单地崩溃即可。


您能试着写出不像like夫一样的答案,而改写更具体并证明您的观点吗?也看看如何回答
LuckyLikey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.