如何吞噬所有异常并保护我的应用程序不崩溃?

10
我发现一些C#应用程序在出现错误条件(例如obj = nullobj.member = null)时会崩溃。很多时候,这个obj来自于第三方应用程序的接口,并导致第三方应用程序和我的Cs应用程序一起崩溃。
我该如何在所有可能出现问题的地方添加异常处理,以便我的应用程序可以在这些灾难性情况下存活?在所有位置添加try-catch并从这种情况中恢复是一个挑战。
我该如何以实际、可靠和防弹的方式实现这一点?
[更新:工业自动化控制]
结构:
GUI(asp.net、c++)- RuntimeApp(C++)- MyCsApp(cs)- 3rdPartyApp(Cs)
正常流程:
1. HostApp--(通过以太网电缆连接)--MyCsApp 2. 操作员--GUI--RuntimeApp--MyCsApp 异常情况:
1.某些非标准操作程序; 2.某些硬件问题发生; 3.等等。
我最好处理所有的异常情况。最重要的是,我必须考虑如何从这些情况中恢复。

17
改修漏洞怎么样? - Aaronaught
9
两个单词。别做。 哎呀,那是三个单词...好吧,现在我可能要说为什么了。 这会阻止你的应用程序在需要时从容退出。 如果你使用下面其他人提到的全局和线程级别的异常处理程序,请计划好在被触发后要做什么... 记录错误很好,包括调用栈将有助于修复错误。 在大多数情况下,让应用程序继续运行可能不是一个好主意...但也可能是...要确保了解你的代码和环境才能确定。 - Jason D
3
很抱歉告诉你,现在已经太晚了。你已经让公司失败了。关注异常处理的时间应该是在设计应用程序的时候。事实上,听到没有为安全考虑而设计工厂自动化软件,我感到很震惊。你是来自丰田吗? - John Saunders
7
如果每一个异常都真正来自第三方应用程序,而不仅仅是处理其数据时的各种错误结果,那么显然您需要将此应用程序视为不可信,并在每个与该应用程序通信的实例中放置错误处理,放在源头处,您可能实际上能够处理它。如果一个NullReferenceException跨越整个堆栈,你就完了,无法优雅地恢复。 - Aaronaught
3
说完这句话后,看了一下你的评论,从第三方应用程序的日志文件中读取不是实现互操作的正确方式。有几种选项可供选择-内存映射文件、命名管道、共享数据库-但如果你直接从日志文件中读取,我怀疑你会遇到许多锁定问题和竞态条件。你提到多线程也说明了同步不足和可能的竞态条件解释是可信的。听起来你需要花费很长时间来调试。 - Aaronaught
显示剩余8条评论
7个回答

19

你不想在任何地方都捕获每个异常。

你想防止异常从应用程序的较低层“泄漏”到可以破坏应用程序或使其崩溃的位置。

但是,防止损坏需要的不仅仅是捕获异常。你需要确保应用程序在可能抛出异常的每个点上始终是安全的以中断应用程序。这可能意味着您需要清理复杂操作。例如:

ComplexBuilder cb = new ComplexBuilder();
try
{
    cb.AddOperation(...);  // Once building starts,
    cb.AddOperation(...);  // it's not safe to use cb
    cb.AddOperation(...);
}
catch (SpecificException ex)
{
    cb.Cleanup();          // until it's cleaned up
}

// Now safe to access cb, whether or not an exception was thrown
我最近遇到一个类似的应用程序。该应用程序的某一部分被认为是“重要”的。当发生这个“重要”的事件时,还应该发生其他事情,但这些事情被认为是“不重要”的。想法是,如果在“不重要”的部分中出现异常,则必须使“重要”的部分继续执行。
结果是,由于某种原因尝试读取资源失败。这将返回null而不是字符串资源。这导致了String.Format调用中的ArgumentNullException。代码捕获了此异常并继续执行。
但在第一个异常和最后一个异常之间,应该分配一个对象,并且应该设置对该对象的引用。但由于异常,设置引用从未发生过。结果是,我看到了NullReferenceException,距离实际问题发生的地方有四个堆栈级别,并且与两个.csproj文件相隔甚远。
因此,当您谈论捕获异常以便让程序继续运行时,您需要记住,捕获所有这些异常会极大地改变程序的控制流。实际上,它可能会改变得如此之多,以至于您不能确定程序是否可以安全地继续执行。

2
叮咚,叮咚,叮咚。我们有一个获胜者。 - mmcdole

7
这是许多开发人员不理解的事情。在异常捕获时,应用程序已经崩溃。发生了一些意外的事情,这意味着您的代码没有预料到它,而且很可能处于不确定状态(即在生成异常时无法确定有多少个有问题的函数已完成,不知道写出了多少数据,设置了硬件的哪些位等)。继续进行安全吗?应该尝试保存用户的数据吗?谁知道!
当您到达高级别的异常捕获时,您没有防止应用程序崩溃。您只是在此时决定如何处理崩溃。您可以放置一个不同于标准消息的消息:

此应用程序执行了非法操作

...但您的自定义消息会说什么更好呢?

我们正在进行未经计划的维护而无警告地关闭,但请放心,这与这个优秀软件中的缺陷没有任何关系

...?


4

你绝对不应该在每个地方都添加try catch语句。

你只需要在顶层捕获所有异常。如果这是一个GUI应用程序,那么只需显示一个漂亮的对话框,上面有一个按钮,上面写着“请报告给支持人员”(它可以将堆栈跟踪快照写到屏幕或文件中)。

如果你很幸运,应用程序可以继续运行(因为你无法知道是否真的出现了严重问题)。

请注意,你也可以这样做。

        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        Forms.Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);

但这并不能阻止它崩溃,只是让你能够捕获失败的信息。


3
首先,治疗疾病是有道理的,找出导致崩溃的原因,确保代码崩溃是因为obj = null或类似情况 - 使用异常处理并吞没所有异常只会掩盖问题...这不是它的用途!听起来很多代码坏味道引发了崩溃 - 保护应用程序免于崩溃并不是正确处理问题的方式,只会让事情变得更糟...

好吧,你可以遵循John Saunders和pm100的建议来做到这一点...但要以一种能够看到根本原因的方式处理它,不要将其视为'神奇的万金油',最终,与第三方应用程序交互的代码需要彻底调试...

例如

object foo = null;
bar baz;
// .... // 现在第三方应用程序设置了foo
if (foo!= null && foo is bar) baz = (bar)foo as bar;
if (baz != null){
//继续,baz是类型'bar'的合法实例
}else{
//优雅地处理它或抛出*用户定义的异常*
}

注意如何使用'as'检查'foo'是否是'bar'实例所需的正确类型 - 现在与此相比较,这是典型的坏味道代码...

object foo = null;
bar baz;
// 现在第三方应用程序设置了foo - 你真的确定它是非空的吗? // 它真的是类型'BAR'吗?
baz = foo; // CRASH! BANG! WALLOP! KERRUNCH!

@tommieb75和Aaronaught。我想接受你们的回复作为我的公司应用解决方案。尝试找到根本原因并修复它。@ALL。我想基于你们的回复进行更多的学习和实践。如果可能的话,我会尝试使用你们的解决方案添加子模块级别的崩溃处理。非常感谢。 - Nano HE

0
如果这是一个Winforms应用程序,请在Main方法中添加以下事件处理程序,如下所示:
Application.ThreadException += Application_ThreadException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

然后您可以显示消息框或其他通知,记录异常等,而无需使应用程序崩溃。


这个编程建议的一个限制是,如果异常发生是由于处于糟糕/无法恢复的状态,那么这实际上会导致应用程序继续表现不良,并且彻底地让用户感到沮丧。当然,处理崩溃的应用程序很令人沮丧,但是处理过度失控的应用程序更加令人沮丧。 - Jason D
同意Jason的观点。我描述的是一个由于个别错误数据而出现问题的LOB应用程序,而不是普遍存在的问题。因此,在捕获异常后,您可以继续愉快地进行操作。您处理的另一组数据可能根本没有任何问题。 - softveda
我不会说你可以“愉快地”继续。至少,这些错误需要被彻底记录、调查、追踪并以合理有序的方式解决。简单地跳过文件或输出加密的调试信息可能会让应用程序继续运行,但会很快拖垮业务。 - Aaronaught
Aaronaught,你误解了“你”的含义。在这种情况下,“你”指的是最终用户。当然,异常处理程序会记录所有内容,而最终用户将通过某些工单系统报告错误。每个支持工单都会进行分析、修复、报告等。我的意思是,对于最终用户来说,应用程序不会崩溃,但在报告错误后,他/她仍可以继续使用它。而你作为开发者,当然有责任修复错误的原因,而不是高高兴兴地将其搁置不理。 - softveda

0

避免此类异常的一种技术是使用空对象模式

因此,您可以使用Account account = Account.SpecialNullAccount;而不是Account account = null;,并在您的Account类中定义SpecialNullAccount作为静态Account对象。现在,如果您尝试在某些无关紧要的代码(比如日志记录代码)中使用account.Name,您将不会得到异常,而是会得到一个值,例如在静态空对象实例上定义的"NO NAME"。

当然,重要的是这样的对象在任何重要情况下都会抛出异常,例如.PayInvoice()或objectContext.SaveChanges(),因此请采取措施确保发生这种情况。


1
@Hightechrider:是的,空对象模式可以防止异常,但它并不能使程序更加正确。最好让异常出现,保护应用程序,然后解决问题。 - John Saunders
@John Saunders。不同意空值模式不会改善您的代码。空值模式可以防止那些本来不必要的异常(例如,log(account.Name)并不是您想要的某个地方出现异常,只是因为用户没有登录),并且它可以更多地揭示您的代码,因为它区分了忘记初始化变量和故意不设置值之间的差异:当PayInvoice()失败时,您可以立即看到是因为账户值从未设置还是因为用户没有登录。 - Ian Mercer
面向对象编程中的空对象模式与数据库中的“NULL”值非常相似-它解决了一些问题,但也带来了大量新问题。它防止了“快速失败”,允许程序长时间运行不正确,并在以后神秘地失败。在这种情况下确保正确性的方法是使用非空类型,例如Spec#实现的类型,但引入此类扩展的时间应该是在代码稳定之后,而不是到处崩溃时。 - Aaronaught

0

AppDomain.UnHandledException和/或AppDomain.ThreadException是您可以订阅以捕获未处理异常的事件。我认为您不能使用它来在离开时继续执行(这也可能不是一个好主意)。它们可以用于吞噬错误消息或记录它等。

然而,是否这样做是个好主意取决于您!


1
这个建议的一个限制是,如果异常发生是由于处于糟糕/无法恢复的状态,那么这实际上会导致应用程序继续表现不良,并且彻底地让用户感到沮丧。当然,处理崩溃的应用程序很令人沮丧,但是处理过度失控的应用程序更加令人沮丧。 - Jason D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接