如何处理未处理的异常?(终止应用程序与保持活动状态)


30

当桌面应用程序中发生未处理的异常时,最佳实践是什么?

我当时想向用户显示一条消息,以便他可以与支持人员联系。我建议用户重新启动应用程序,但不要强行执行。与此处讨论的内容类似:ux.stackexchange.com-处理意外的应用程序错误的最佳方法是什么?

该项目是.NET WPF应用程序,因此所描述的提案可能看起来像这样(请注意,这是一个简化的示例。可能有必要隐藏异常详细信息,直到用户单击“显示详细信息”并为轻松报告错误):

public partial class App : Application
{
    public App()
    {
        DispatcherUnhandledException += OnDispatcherUnhandledException;
    }

    private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        LogError(e.Exception);
        MessageBoxResult result = MessageBox.Show(
             $"Please help us fix it and contact support@example.com. Exception details: {e.Exception}" +
                        "We recommend to restart the application. " +
                        "Do you want to stop the application now? (Warning: Unsaved data gets lost).", 
            "Unexpected error occured.", MessageBoxButton.YesNo);

        // Setting 'Handled' to 'true' will prevent the application from terminating.
        e.Handled = result == MessageBoxResult.No;
    }

    private void LogError(Exception ex)
    {
        // Log to a log file...
    }
}

然后,在实现中(ViewModels的命令或外部事件的事件处理程序),我将仅捕获特定的外部异常,并使所有其他异常(骨头异常和未知异常)冒泡直至上述“最后处理程序”。有关笨拙和外来异常的定义,请查看:Eric Lippert-令人讨厌的异常

让用户决定是否应终止应用程序是否有意义? 当应用程序终止时,您可以确定没有不一致的状态...另一方面,用户可能会丢失未保存的数据,或者无法再停止任何已启动的外部进程,直到应用程序重新启动为止。

还是决定是否根据编写的应用程序类型在未处理的异常上终止应用程序?是否仅在“鲁棒性”与“正确性”之间进行权衡,如“ 代码完成”第二版中所述

为了给您一些上下文,我们正在谈论哪种应用程序:该应用程序主要用于控制化学实验室仪器并将测量结果显示给用户。为此,WPF应用程序与某些服务(本地和远程服务)进行通信。WPF应用程序不直接与仪器通信。


27
如果您不希望发生异常,那么如何确定应用程序可以安全地继续工作呢?
重复数据删除器

2
@Deduplicator:当然,您不能确定。就像已经写过对Matthew的回答的注释:“是的,应用程序当然可能处于无效状态。也许某些ViewModel的状态只有部分更新。但这会不会有任何危害?用户可以重新加载数据,如果有无效的话,发送给该服务,则该服务将始终无法接受。如果用户能够在重新启动应用程序之前进行保存,对用户来说是否更好?”
乔纳斯·本茨

2
@Voo因此,您可以确保始终期待异常,从而确保应用程序可以安全地继续运行吗?似乎您否认接收到意外异常的前提。
重复数据删除器

2
无论如何,请使错误消息可复制。或者,说出它是在哪个日志文件中写入的。
ComFreek

2
处理不一定暗示显式操作。如果可以确定应用程序可以安全继续运行,则说明已处理了该异常。
chepner

Answers:


47

您必须期望程序终止的原因更多,而不仅仅是未处理的异常,例如电源故障或使整个系统崩溃的其他后台进程。因此,我建议终止并重新启动应用程序,但是要采取一些措施来减轻这种重新启动的后果最大程度地减少可能的数据丢失

首先分析以下几点:

  • 如果程序终止,实际上会丢失多少数据?

  • 这样的损失对用户而言到底有多严重?是否可以在不到5分钟的时间内重建丢失的数据,还是我们正在谈论丢失一天的工作?

  • 实施“中间备份”策略需要付出多少努力?不要排除这一点,因为正如您在评论中所写的那样,在常规的保存操作中“用户必须输入更改原因”。最好考虑一下临时文件或状态之类的东西,它们可能在程序崩溃后自动重新加载。许多类型的生产力软件都可以做到这一点(例如,MS Office和LibreOffice都具有“自动保存”功能和崩溃恢复功能)。

  • 如果数据有误或损坏,用户是否可以轻松查看(也许在程序重新启动后)?如果是,则可以提供一个选项,让用户保存数据(极有可能损坏数据),然后强制重新启动,重新加载它,并让用户检查数据是否正常。确保不要覆盖定期保存的最新版本(而是写入临时位置/文件),以免破坏旧版本。

如果这样的“中间备份”策略是明智的选择,则最终取决于应用程序及其体系结构以及所涉及数据的性质和结构。但是,如果用户的工作时间少于10分钟,并且这样的崩溃每周发生一次,甚至很少发生,那么我可能不会对此投入太多的精力。


10
en.wikipedia.org/wiki/Crash-only_software,这就是Android应用程序按需工作的方式。
o鸭

3
出色的答案-以及一个在更广泛的上下文中考虑问题的好例子(在这种情况下,“在任何崩溃情况下如何防止数据丢失?”)可以带来更好的解决方案。
sleske,

1
我做了一个小小的修改以指出您不应覆盖旧数据-希望您不要介意。
sleske,

1
@MooingDuck很多Android应用程序(例如游戏)在崩溃时都会失去状态。
user253751

1
@immibis:是的,Android确实有大量非常低质量的应用程序。
Mooing Duck

30

在某种程度上,它取决于您正在开发的应用程序,但总的来说,我想说的是,如果您的应用程序遇到未处理的异常,则需要将其终止。

为什么?

因为您不能再对应用程序的状态充满信心了。

当然,向用户提供有用的信息,但是您最终应该终止该应用程序。

给定您的上下文,我绝对希望该应用程序终止。您不希望在实验室中运行的软件产生损坏的输出,并且由于您不打算处理该异常,因此您不知道为什么抛出该异常以及发生了什么。


我试图在最后一部分中添加有关该应用程序的一些上下文信息。
Jonas Benz

10
@JonasBenz 如果用户能够在重新启动应用程序之前进行保存,对用户来说不是更好吗? 是的,但是您如何知道用户要保存的数据是否有效并且没有损坏?在这一点上,您有一个意外的异常,您真的不知道为什么。尽管最烦人的是您最安全的方法是终止应用程序。如果您担心用户节省工作,则可以采用恒定节省策略。同样,这完全取决于您正在编写的应用程序。
马修

4
是的,我可以在这里以同样的方式争论:我不同意继续按钮的存在。问题很简单,如果您(应用程序开发人员)不知道您是否可以安全继续,那么用户如何知道?如果遇到未处理的异常,则意味着您有一个未曾预料到的错误,并且无法确定此时的情况。我知道用户希望继续,因为他们不想丢失他们的工作,但是即使您的应用程序由于此错误而产生不好的结果,您是否也想让他们继续?
马修

3
@Matthew “如果您,应用程序开发人员不知道您是否可以安全地继续操作,用户怎么知道”,开发人员不知道他们何时编写代码。当用户遇到这样的特定错误时,它可能是已知的。用户可能会从任何用户论坛,支持渠道等渠道找到答案,或者只是通过测试并查看其数据发生了什么...我同意,作为用户功能仍然有些晦涩难懂,只是指出时间使它变得危险用户确实可以知道“继续”是否明智。
海德

1
@JonasBenz,在Windows 3.1下,当程序执行非法内存访问时出现的对话框带有一个“忽略”按钮,使程序可以继续运行。您会注意到,Windows的每个后续版本都没有该按钮。
标记

12

考虑到这是针对化学实验室的,并且您的应用程序不直接控制仪器而是通过其他服务来控制:

显示消息后强制终止。发生未处理的异常后,您的应用程序处于未知状态。它可能会发送错误的命令。它甚至可以引起鼻恶魔。一个错误的命令可能会浪费昂贵的试剂或带来危险的设备或人

但是您可以执行其他操作:重新启动后正常恢复。我假设您的应用程序崩溃时不会自行关闭这些后台服务。在这种情况下,您可以轻松地从它们中恢复状态。或者,如果您有更多状态,请考虑保存它。在具有数据原子性和完整性规定的存储中(可能是SQLite?)。

编辑:

如评论中所述,您控制的过程可能需要足够快的更改,以使用户没有时间做出反应。在这种情况下,除了正常状态恢复外,您还应考虑静默重启应用程序。


在需要后续命令的状态下立即终止在化学实验室中可能同样危险。
Oleg V. Volkov,

@ OlegV.Volkov,所以也许在终止时重新启动自己?在运行良好的计算机上,GUI的启动时间大约为数百毫秒。如果该过程需要更严格的计时,则不会在非实时操作系统上实现控制。尽管应该由OP进行最终风险评估。
Jan Dorniak

@ OlegV.Volkov但这是一个好点,因此我在答案中添加了我的观点。
Jan Dorniak

8

试图在程序的顶层普遍回答这个问题不是明智的选择。

如果有什么事情冒出来,并且在应用程序体系结构的任何地方都没有人考虑过这种情况,那么您就无法对可以采取或不采取什么措施进行概括。

因此,不,绝对不允许用户选择应用程序是否尝试恢复的设计,因为该应用程序和开发人员没有进行必要的尽职调查,以查明是否可行或明智,这绝对不是让用户选择的设计。

但是,如果应用程序的逻辑或行为的高价值部分已经考虑到了这种故障恢复而设计,并且在这种情况下可以利用它们,那么一定要这样做-在这种情况下,提示用户查看是否要尝试恢复,或者是否只想退出并重新开始,可能是可以接受的。

通常,对于所有(或什至大多数)程序来说,这种恢复都不是必需的,也不是明智的选择,但是,如果您正在开发一个需要这种程度的操作完整性的程序,则可能是出现这种恢复的情况。向用户提示将是一件理智的事情。

在任何特殊的故障恢复逻辑中-不,不要这样做。您实际上不知道会发生什么,否则,您将进一步捕获并处理该异常。


不幸的是,许多诸如“用从某个位置接收到的数据构建对象”之类的方法,都没有区分表示该操作无法完成的异常,但是这种尝试没有副作用,而那些表示更严重的异常的方法之间没有区别。出了错。如果通常出于无法构造对象的准备而进行了尝试,则由于某种未曾预料到的原因而导致加载资源失败的事实,不应导致致命错误。重要的是副作用,但是不幸的是副作用框架忽略了这些事情。
超级猫

@supercat-如果可以识别错误,则可以处理。如果您无法识别它,就无法处理它,除非您编写例程以对应用程序的状态进行完整性检查,以试图找出可能出了什么问题。错误“可能是什么”无关紧要,我们已经明确地确定,由于我们通常试图处理未捕获的异常,因此我们不知道这一点。
Iron Gremlin

3

“异常异常”(即您尚未预见到的异常)的问题在于您不知道程序处于哪个状态。例如,尝试保存用户的数据实际上可能会破坏更多的数据

因此,您应该终止该应用程序。

George Candea和Armando Fox提出了一个非常有趣的想法,称为“仅崩溃软件”。这个想法是,如果您以这样一种方式设计软件,使其关闭的唯一方法是将其崩溃,并且将其启动的唯一方法是从崩溃中恢复,那么您的软件将更具弹性,并且错误恢复代码路径将进行更彻底的测试和实践。

他们注意到某些系统在崩溃后比有序关闭后启动得更快,因此提出了这个想法。

一个很好的(虽然不再相关的例子)是一些旧版本的Firefox,它们不仅在崩溃崩溃后启动速度更快,而且还具有更好的启动体验!在这些版本中,如果您正常关闭Firefox,它将关闭所有打开的选项卡,并以一个空选项卡启动。从崩溃中恢复时,它将在崩溃时恢复打开的选项卡。(这是在不丢失当前浏览上下文的情况下关闭Firefox的唯一方法。)那么,人们做了什么?他们只是从不关闭Firefox,而总是pkill -KILL firefox编辑它。

Valerie Aurora在Linux Weekly News上一篇关于仅崩溃软件的不错的文章。这些评论也值得一读。例如,评论中的某人正确地指出这些思想不是新思想,实际上或多或少等同于基于Erlang / OTP的应用程序的设计原理。而且,当然,今天来看一下,距瓦莱丽(Valerie)已有10年,而距原始文章又有15年,我们可能会注意到,当前的微服务炒作正在重新发明这些相同的想法。现代云规模的数据中心设计也是一个更粗粒度的例子。(任何计算机都可以随时崩溃而不会影响系统。)

但是,仅让软件崩溃是不够的。它必须为此而设计。理想情况下,将您的软件分解为各个独立的小型独立组件。另外,“崩溃机制”应位于要崩溃的组件之外。


1

处理大多数异常的正确方法应该是使可能因此处于损坏状态的任何对象无效,并在无效对象不能阻止这种情况的情况下继续执行。例如,更新资源的安全范例将是:

acquire lock
try
  update guarded resource
if exception
  invalidate lock
else
  release lock
end try

如果在更新受保护的资源时发生意外的异常,则应假定该资源处于损坏状态,并且该锁无效,无论该异常是否属于良性类型。

不幸的是,每当受保护的块退出时,通过IDisposable/ 实现的资源保护措施using都会被释放,而不会知道该块是正常退出还是异常退出。因此,即使应该为异常后何时继续进行定义明确的标准,也无法告诉它们何时适用。


+1只是为了表达关于正确方法的相对不明显但仍不常见的观点。我实际上还不知道是否同意,因为对我来说这是一种新颖的启发式/规则,因此我不得不考虑一会儿,但这似乎是明智的。
mtraceur

0

您可能会使用每个iOS和MacOS应用程序都遵循的方法:未捕获的异常会立即关闭应用程序。再加上许多错误,例如数组越界或仅在较新的应用程序中发生算术溢出,都可以做到这一点。没有警告。

根据我的经验,许多用户没有注意到任何东西,只是再次点击应用程序图标。

显然,您需要确保此类崩溃不会导致重大数据丢失,并且绝对不会导致代价高昂的错误。但是警告“您的应用程序现在将崩溃。如果不打扰您,请致电支持人员。

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.