异常处理——我应该抓住这个异常吗?

目录

介绍

不好的例子——请不要写这样的代码

您发现错误了吗?

可能的调用结果

探索情况

直接返回值(或无效)

包含错误代码的返回值

可处理的异常

前置条件异常

后置条件异常

断言——另一种抛出异常的方式

异步异常

我们什么时候应该捕捉异常?

主应用循环与本地异常处理

结论


介绍

在本文中,我将专注于C#.NET如何处理异常以及何时捕获它们。它可能与其他语言如何使用它们有关(我确信它在某种程度上类似于JavaC++中的异常),但它与Python使用异常的方式绝对不同,因为它用于停止迭代器,在C#中我们可以但绝不应该做的事情。

不好的例子——请不要写这样的代码

看一下这段代码:

var cachedNames = new string[3];
try
{
  PopulateCachedNames(cachedNames);
}
catch
{
  // Just ignore any exceptions.
}

UseOurPopulatedCache(cachedNames);

PopulateCachedNames 可能是一个非常复杂的方法,但为了本文的简洁,我们假设这是它的代码:

private static void PopulateCachedNames(string[] array)
{
  for (int i=1; i<=array.Length; i++)
    array[i] = i.ToString();
}

您发现错误了吗?

数组索引从0Length-1(或小于Length)开始,但我编写了for循环从 1Length,这意味着我从未填充索引0,而且此代码尝试访问的最后一个索引是无效的。

.NET不允许我们访问无效的数组索引和损坏的内存。因此,它给了我们一个安全的异常,而不是导致任何内存损坏。这样的例外是为了让我们知道我们做错了什么,因为在这种情况下语言可能什么都不做,但这只是隐藏错误而不是让它们可见,因为代码仍然会关闭1错误

然而,异常可以被捕获,在这个样本中,我正是这样做的,让cachedNames数组大部分被填充。我可以更进一步说,根据方法UseOurPopulatedCache使用数组的方式,这可能不会有其他问题。

然而,如果我们在循环中使用正确的索引,我们只是忽略了一个可以轻松修复的异常。那不是更好吗?而且,假设我们修复了循环,我们应该保留那个try/catch以防万一吗?

由于我不想留下任何疑问,因此答案分别是yesno。我们应该修复代码,我们不应该主动尝试以防万一捕获异常。

可能的调用结果

专注于C#,假设异常是调用的替代结果,我们在调用方法时可能会得到什么样的结果?

如果问题的答案只是有效的结果而不是其他任何东西,我会很高兴,但不幸的是,我看到的情况更像这样:

  • 直接返回值(或void
  • 返回包含错误代码的值
  • 可处理的异常 -Red Flag
  • 前置条件异常
  • 后置条件异常
  • 断言——另一种抛出异常的方式
  • 异步异常

当然,还有更多的结果,比如整个操作系统崩溃,但我绝对不会关注这些奇怪的情况,因为我真正关注的是返回和C#/.NET提供的托管异常。

所以,回到我刚刚提出的列表,我可以说结果似乎属于以下三个类别中的任何一个:

  1. 预期的有效结果——仅返回值
  2. 预期的无效结果——错误代码和可处理的异常
  3. 这绝不应该在已发布的应用程序上发生——所有其他应用程序

而且,对于这永远不会发生的情况,我可以将其细分为这永远不会发生——这是我自己的代码做错了这永远不会发生——我的代码的用户做错了什么

独立于为什么发生不应该发生的情况,最好只记录事件并让应用程序死掉。

说真的......处理不应该发生的事情的异常意味着事情是错误的,并且有可能通过捕获异常来保持应用程序运行将导致越来越多的损坏状态......在某些时候严重崩溃。

探索情况

让我们探索每种情况:

直接返回值(或无效)

这实际上是最常见的情况,并且大部分时间都会发生。我们调用一个函数、方法或获取或设置一个属性值,一切就正常了。如果我们期望返回值,它们就是我们想要的返回值,而不是错误代码或类似的东西。

包含错误代码的返回值

在这种情况下,方法或函数显然有一个返回值,准备给用户至少一个基本的指示,即预期的调用失败。这可以通过将魔术值作为返回值(例如在仅0预期值或正值时返回-1)或返回更复杂的类型来实现。在任何情况下,无论结果中包含多少错误信息,它都可以被视为有效无效,而无需处理异常或替代返回路径

可处理的异常

这是有效结果的最后一种情况,但我会将其放在一个非常特殊的类别中,名为我们不应该这样做,除非我们被现有框架强迫这样做

好的......也许这太苛刻了......但我关注的是生成异常的成本很高并且破坏正常返回值的事实。在不好的地方生成异常,并没有预料到会出现异常,这意味着我们可能会留下损坏的数据,但正如我在文章开头所说,Python使用异常来停止迭代器,同样可以在. NET中这样做,如果我们想要......所以,它们可能会按预期工作,但是,我会认真地要求没有人这样做,除非,也许,作为一个练习。

我不会争辩说在C#中使用try/catchtry/finally或仅使用using子句可以帮助我们避免数据损坏。如果抛出异常,所有这些异常处理机制确实可以帮助我们当前的代码表现良好,但它们并不能保证外部代码的正常表现。例如,我知道许多C++库只是禁用异常处理,因为它没有按预期工作。这意味着如果这样的库是我们应用程序的一部分并且它调用了抛出的C#方法,它只会杀死应用程序(更糟糕的是,可能有一个非常神秘的调用堆栈)。所以......假设我们创建了可处理的异常,我们可能会重新考虑这一点,并只使用包含错误代码的返回值。

进一步探索这一点,我总是想起Stream类,因为它在WriteRead方法之间的差异。Read返回一个int,告诉实际读取了多少数据,这可能小于我们要求的数量,也可以返回0来告诉Stream结束,或返回负值以指示错误。Writevoid,所以它通常不会返回,但是,如果发生错误——比如磁盘已满,或者连接丢失——就会抛出异常。

虽然我可以理解为什么这被视为一个好主意,但我更喜欢有2组方法,像WriteRead返回void并在需要时抛出,以及另一组没有异常的方法,如TryReadTryWrite,返回实际读取的数据量或写入或返回错误代码。

除了这些异常的危险之外,它们仍然存在,也许您仍然更喜欢使用它们来保持大部分代码的干净,避免有很多“ifs”来处理非常罕见的错误情况。假设我们有这样的例外,我会说这些是我们唯一可以(并且可能应该)在不重新抛出的情况下捕获的例外。

前置条件异常

这些是最常见的异常,至少当我们谈论基类库或在继续之前进行适当验证的代码时,所以我会说它们主要是在验证输入参数时引起的异常。

例如:我们创建一个List。添加5个项目(索引从04...如果我们尝试访问索引-1 或更低,或者索引5或更高,我们会得到一个异常。

对于一些开发人员来说,这很烦人,特别是如果它只是一个一个接一个的情况会扼杀我们正在运行的应用程序。但是这样的异常实际上是在告诉我们我们做错了什么,一旦我们的代码被修复,我们就会摆脱所有这些异常。

所以,假设我们的代码是正确的,这些情况永远不会发生,也不会抛出异常。我们可以将它们视为有用的验证,告诉我们一旦犯错就做错了(而不仅仅是破坏记忆并让坏事发生)。我们是否应该catch这样的异常来保持应用程序运行?

假设这些都是前置条件,抛出异常的调用可能不会破坏任何状态,在catch之后继续使用对象是可以的。然而,如果我们确定我们没有传入无效的参数并且我们得到了异常,那么我们就有一个严重的问题需要解决。我们的代码,或者我们正在调用的代码,都被窃听了。

当抛出异常的对象超出范围时捕获异常可能没问题(我仍然会避免它),但如果我们仍然要再次使用该对象,我们很有可能只是破坏(更多)数据并且我们不会真正保持应用程序运行。相反,我们只是让我们更难发现真正的问题发生在哪里。那么,我们应该捕获这些异常吗?

后置条件异常

后置条件异常意味着我们在调用完成后验证状态。例如,一个永远null不应该返回但实际上返回的方法可能被视为违反后置条件,并且编程语言可以将其转换为异常。

老实说,我没有看到很多语言资源来验证这些后置条件,而是看到一些调用者在每次调用时验证其方法调用的结果,可能使用辅助方法来进行所需的验证,或者完全忽略结果可能是错误的事实。如果一个方法已经返回了它不应该返回的东西,那么在这里异常明智没有什么可做的,除了如果我们可以访问它的源代码来修复这样的方法。

无论如何,如果我们实际上有后置条件异常,这意味着代码在完成一个操作后刚刚检测到一些错误,这很可能意味着该方法存在错误,并且我们现在有一个具有一些无效内部状态的对象。

这是一种不应该发生的情况,因为这意味着被调用的代码只是做错了什么。如果我们所有的输入值都被正确验证,我们的代码正在工作,我们是如何得到任何无效结果或状态的?而且,如果无效值是使用调试器强制放置的,我们真的可以做些什么来继续使用这个对象吗?

如果不是,至少当前对象需要死亡。但是假设我们不知道方法调用的内部工作原理,我们不知道它是依赖于静态状态还是与其他对象交互,所以可能会有更多不好的事情发生。

断言——另一种抛出异常的方式

大多数情况下,断言只是生成异常的另一种方式。同样令人困惑的是,许多单元测试框架使用相同的名称(即assert),但目的有所不同。

在单元测试中,断言是测试验证条件并允许测试通过或失败的方式。它们是测试特定的东西,不会影响非单元测试代码。

在单元测试之外,断言通常用于检查对象的内部状态是否正常。这与正常的前置条件和后置条件非常相似,但是,我们不是验证输入参数是否有效,而是验证对象的内部字段是否处于良好状态。

当一个断言失败时,它通常会产生一个异常。我说通常是因为不同的断言实现可以做不同的事情,比如显示一个带有忽略它的选项的窗口,只是立即终止应用程序等等。

就个人而言,如果一个断言正在做一个有效的检查(而不仅仅是一个坏断言),当它失败时忽略它是很危险的……但这通常是大多数发布版本中发生的情况。为了让代码运行得更快,断言检查通常不在发布版本中编译。然而,这样做,如果这些发布版本中确实发生了糟糕的情况,我们可能会丢失非常有用的信息。

在发布版本上禁用断言是一个有争议的话题,它可能值得单独写一篇文章——请记住,断言可能不会在特定版本上运行。

无论如何:如果我们的断言只是证明我们的对象已损坏,我们可以继续前进吗?我们应该catch异常并继续使用已知损坏的对象吗?

异步异常

当抛出异常并且它甚至不是由我们调用的代码生成时,就会发生异步异常。.NET中异步异常的最佳示例是ThreadAbortException,当调用Thread.Abort()时会抛出异常。尽管我们可以捕获ThreadAbortException甚至完全取消abort调用,但.NET本身的编写方式并不能保证我们最基本的对象在发生这种情况时会处于正常状态。所以,假设我们从不同的线程得到这样的异常并且面临任何被破坏的可能性......我们真的应该继续吗?捕获此异常并取消它是个好主意吗?

我们什么时候应该捕捉异常?

我认为在看到我的列表之后,可能会想到的一件事是我们不应该捕获异常

而且......好吧,我主要是说......但我真正的意思是我们几乎不应该处理异常。也就是说,在大多数情况下,捕获异常而不重新抛出它是一件坏事。而且,如果我们不重新抛出它们,它们通常需要成为例外处理。

尽管异常本身没有说明,但异常的目的和文档可能清楚地表明它们可以被处理,就像Stream.Write抛出IOException异常一样(但不是其他异常,比如ArgumentNullException)。

主应用循环与本地异常处理

我曾经做过一次面试,面试官喜欢我的代码抛出无效的输入参数这一事实,但不喜欢我的代码没有任何try/catch块。根据面试官的说法,主应用程序循环应该有try/catch块来处理任何异常,并在发生意外情况时让应用程序继续运行。

有趣的是,如果我的代码没有任何try/catch子句,只要我的代码没有执行throw。就像,如果我访问了一个数组的无效索引,那么这样的异常没有被捕获是可以的,因为它不是我的代码

像这样的观点,即我们的代码需要与主要的.NET代码表现不同,这对我来说简直是天方夜谭。如果它因完全相同的原因而失败,但在一种情况下需要处理而在另一种情况下不需要处理,这意味着,对于同一个问题,我们只是基于谁编写了代码而使用了两种不同的度量,而不是基于代码正在做什么而使用单一的度量。

无论如何,有一个主要的地方来处理所有异常的想法,比如最接近主应用程序循环而不是真正发生异常的地方,只会增加坏事发生的机会。

是完全可以有一个通用的异常处理程序捕获所有的异常如果我们只是记录错误和戒烟,但它不是OK只是丢弃异常并保持运行,好像什么事也没有发生,当我们不知道为什么第一时间的异常被抛出。这样做可能对小的应用有效,特别是当人们开始生成异常而不是有适当的错误返回机制时,但是我们导入的外部库越多,应用变得越大,完全破坏应用内存的可能性就变得越来越现实。

如果主循环机制可能需要处理任何随机异常并仍然保持应用程序运行,我们应该考虑使用AppDomainAssemblyLoadContext能够在发生未知异常时卸载然后重新加载坏库。

结论

我们应该避免仅仅确保应用程序不会崩溃来处理异常。为此,我们能做的最好的事情就是编写无错误的代码。

如果我们不知道为什么抛出异常,最好让应用程序崩溃,如果我们知道,我们应该通过使用可能提供错误代码的替代方法,或者通过在距离它被抛出的最近的地方,而不是在通用解决方案中。如果应用程序确实需要通用解决方案来保持运行,例如Web服务器或类似的,请使用AppDomainAssemblyLoadContext寻找替代方案,因为它们可能会更好地工作,并且会避免由于损坏状态而导致坏事发生对象还活着。

https://www.codeproject.com/Articles/5316261/Exception-Treatment-Should-I-Catch-this-Exception

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值