在使用foreach循环时,使用try-catch块的最佳实践是什么?

12

关于性能,使用 try{} catch{} 块的最佳实践是什么?

foreach (var one in all)
{
    try
    {
        //do something
    }
    catch { }
}

或者
try
{
    foreach (var one in all)
    {
        // do something
    }
}
catch { }

2
任何你可以移出循环的东西通常都对性能有好处。但它们并不完全等价,因为访问“all”可能会抛出异常,并且在两者之间的处理方式也可能不同。此外,在第一个中抛出异常可能会根据catch中的操作而继续循环。 - Joachim Isaksson
1
它们是不同的,也就是说,执行的可能路径是不同的。这就像在问哪个更好:if(){ for(){} } vs for(){ if(){} }:它们有不同的用途。 - clcto
2
@JoachimIsaksson 特别是当它可以加速代码的时候。;) - PTwr
@PTwr 现在这就有点奇怪了 :) - Joachim Isaksson
@JoachimIsaksson 如果你从汇编的角度来看,就不是这样的(已经有很好的解释了,所以我就不在这里烦扰了)。 - PTwr
显示剩余3条评论
7个回答

16

公平地说,没有硬性规定,这取决于具体情况。

这取决于你是否想在其中一个项目出现问题时停止整个循环,或者只是捕获该单个问题并继续。

例如,如果你正在向人们发送电子邮件,如果发送其中一个电子邮件时发生异常,你不希望停止处理,但如果你正在管理一组数据库事务,并且需要回滚任何失败的事务,也许更希望在异常/问题发生时停止处理吧?


1
我应该补充一点——如果循环中不需要try catch,而可以将其放在外面,那么这样会更好,因为它能减少复杂性。 - Daniel Dawes

10
根据要求,这是我的精彩答案。有趣的部分将在最后,因此如果您已经知道 try-catch 是什么,请随意滚动。 (抱歉,部分不相关)
让我们先回答一般情况下 try-catch 的概念。
为什么?因为这个问题表明了缺乏如何以及何时使用这个功能的全面知识。
什么是 try-catch?或者更确切地说,什么是 try-catch-finally?
(本章也称为:你为什么还没有用 Google 学习它?)
1. Try - 可能不稳定的代码,这意味着您应该将所有稳定的部分移出它。它总是执行,但不能保证完成。
2. Catch - 在这里,您可以放置设计用于纠正在 Try 部分中发生的故障的代码。只有当在 Try 块中发生异常时才会执行。
3. Finally - 它是第三部分,也是最后一部分,在某些语言中可能不存在。它总是被执行。通常用于释放内存和关闭 I/O 流。

一般来说,try-catch是将潜在不稳定代码与程序的其余部分分离的一种方式。 在机器语言方面,它可以缩短为将所有处理器寄存器的值放置在堆栈上以保存它们免于损坏,然后通知环境忽略执行错误,因为它们将由代码手动处理。

使用try-catch块的最佳实践是什么?

根本不使用它们。用try-catch覆盖代码意味着您期望它失败。为什么代码会失败?因为它编写得很差。 编写不需要try-catch即可安全工作的代码,这对性能和质量都更好。

有时,特别是在使用第三方代码时,try-catch是最简单和最可靠的选择,但大多数情况下,在自己的代码中使用try-catch表示存在设计问题。

例子:

  1. 数据解析 - 在数据解析中使用try-catch非常糟糕。有许多安全解析奇怪数据的方法。其中最丑陋的方法之一是正则表达式(遇到问题?使用正则表达式,问题会变得更多)。字符串转换为整数失败?首先检查您的数据,.NET甚至提供了TryParse等方法。

  2. 除以零、精度问题、数值溢出 - 不要用try-catch来覆盖它,而是升级您的代码。算术代码应该从好的数学方程开始。当然,您可以大幅修改数学方程以使其运行更快(例如通过0x5f375a86),但您仍然需要良好的数学基础。

  3. 列表索引超出范围、堆栈溢出、分段故障、心脏出血 - 这里您的代码设计存在更大的错误。这些错误不应该在健康环境下正常编写的代码中发生。所有这些都归结为一个简单的错误,即代码确保索引(内存地址)在预期边界内。

  4. I/O错误 - 在尝试使用流(内存、文件、网络)之前,第一步是检查流是否存在(非空,文件存在,连接打开)。然后,您检查流是否正确 - 您的索引是否在其大小内?流是否可以使用?它的队列/缓冲器容量是否足够大以容纳您的数据?所有这些都可以在没有单个try catch的情况下完成。特别是当您在框架(.NET、Java等)下工作时。

    当然,仍然存在意外访问问题的问题 - 鼠标咬了您的网络电缆,硬盘驱动器融化了。在这里,try-catch的使用不仅可以原谅,而且应该发生。但是,它需要以适当的方式进行,例如文件的此示例。您不应该将整个流操作代码放在try-catch中,而应使用内置方法来检查其状态。

  5. 糟糕的外部代码 - 当您开始使用可怕的代码库时,没有任何纠正它的手段(欢迎来到企业世界),try-catch通常是保护其余代码的唯一方法。但是,只有直接危险的代码(调用糟糕编写的库中的可怕函数)应该放在try-catch中。

那么什么时候应该使用try-catch,什么时候不应该使用?

这可以用一个非常简单的问题来回答。

我能否纠正代码,使其不需要try-catch?

是的?那就放弃try-catch并修复您的代码。

否?然后将不稳定的部分打包在try-catch中,并提供良好的错误处理。

如何在Catch中处理异常?

第一步是了解可能发生的异常类型。现代环境提供了一种易于将异常分离为类的方法。尽可能捕获最具体的异常。进行I/O操作?捕获I/O异常。进行数学运算?捕获算术异常。

用户应该知道什么?

只有用户可以控制的内容:

  • 网络错误 - 检查您的电缆。
  • 文件I/O错误 - 格式化c:。
  • 内存不足 - 升级。

其他异常只会告诉用户您的代码编写得有多糟糕,因此请坚持神秘的“内部错误”。

在循环中还是循环外使用try-catch?

正如许多人所说,对于这个问题没有定论。这完全取决于您提交的代码。

通用规则可以是: 原子任务,每个迭代都是独立的-在循环内放置try-catch。 链式计算,每个迭代都依赖于前面的迭代-在循环外放置try-catch。
for和foreach有何不同?
foreach循环不能保证顺序执行。听起来很奇怪,几乎不会发生,但仍然有可能。如果您使用foreach进行其创建的任务(数据集操作),则可能希望在其周围放置try-catch。但是正如所解释的那样,您应该尽量避免过多地使用try-catch。
有趣的部分!
这篇文章的真正原因只是来自您的几行话,亲爱的读者!
根据Francine DeGrood Taylor的要求,我将在有趣的部分写更多内容。请记住,正如Joachim Isaksson注意到的那样,这看起来非常奇怪。
尽管本节重点介绍.NET,但它也适用于其他JIT编译器,甚至部分适用于汇编语言。
那么..为什么将try-catch放置在循环周围能够加速循环呢? 这根本没有任何意义!错误处理意味着额外的计算!

请查看这个关于它的Stackoverflow问题:Try-catch speeding up my code?。你可以在那里阅读与.NET相关的内容,这里我将尝试集中讨论如何滥用它。

请注意,这个问题来自2012年,因此它也可能在当前的.NET版本中被“纠正”(它不是一个错误,而是一个特性!)。

如上所述,try-catch将代码片段与其余部分分离开来。分离的过程类似于方法,因此,您也可以将具有重型计算的循环放置在单独的方法中,而不是使用try-catch。

为什么分离代码可以加速它?寄存器。网络比HDD慢,HDD比RAM慢,RAM与超快速的CPU Cache相比也很慢。还有CPU寄存器,它们嘲笑Cache的速度慢。

分离代码通常意味着释放所有通用寄存器-这正是try-catch正在做的事情。或者更确切地说,是JIT由于try-catch而执行的操作。

JIT最显著的缺陷是缺乏预知性。它看到循环,就编译循环。当它最终注意到循环将执行数千次并进行使CPU嘎吱作响的计算时,释放寄存器已经太晚了。因此,循环中的代码必须编译以使用剩余的寄存器。
即使一个额外的寄存器也可以产生巨大的性能提升。每个内存访问都非常耗时,这意味着CPU可能会闲置相当长的时间。虽然现在我们有乱序执行、可爱的管道和预取操作,但仍然存在强制代码停止的阻塞操作。
现在让我们谈谈为什么与x64相比,x86很糟糕且垃圾。在链接的SE问题中,try-catch的速度增益在编译为x64时没有发生,为什么?
因为一开始根本没有速度增益。所有存在的只是由于垃圾JIT输出引起的速度损失(经典编译器没有这个问题)。Try-catch主要是意外地纠正了JIT行为。

x86寄存器是为了特定任务而创建的。x64架构将其大小加倍,但这仍然无法改变一个事实,即在执行循环时,您必须牺牲CX,其他寄存器也是如此(除了可怜的BX)。

那么为什么x64如此出色呢?它拥有8个额外的64位宽寄存器,没有任何特定用途。您可以将它们用于任何事情。不仅理论上像x88寄存器一样,而且确实可以用于任何事情。八个64位寄存器意味着直接存储在CPU寄存器中的八个64位变量,而不需要为进行数学运算(通常需要AX和DX作为结果)而使用RAM。64位意味着什么?x86可以将Int装入寄存器,而x64可以将Long装入寄存器。如果数学块有空寄存器来工作,它就可以在不触及内存的情况下完成大部分工作。这就是真正的速度提升。

但这并不是结束!您还可以滥用缓存。缓存越接近CPU,速度就越快,但它也会更小(成本和物理大小是限制)。如果您将数据集优化为一次填充缓存,例如以L1的一半大小为单位的日期块,将另一半留给代码和CPU在缓存中发现必要的任何内容(除非您使用汇编语言,否则您无法真正优化它,在高级语言中,您必须“估算”)。通常,每个(物理)核心都有自己的L1内存,这意味着您可以同时处理几个缓存块(但创建线程的开销并不总是值得的)。
值得一提的是,旧版Pascal/Delphi在32位处理器时代在多个重要功能中使用了“16位恐龙”(使它们比来自C/C++的32位函数慢两倍)。所以请爱护您的CPU寄存器,即使是可怜的老BX。他们非常感激。
为了补充一些内容,因为这篇文章已经变得相当疯狂了,为什么C# / Java可以同时比本地代码慢和快?JIT是答案,框架代码(IL)被转换为机器语言,这意味着长的计算块将执行与C / C ++的本地代码一样。但请记住,您可以轻松地在.NET中使用本机组件(在Java中尝试这样做可能会让您发疯)。对于足够复杂的计算,您可以通过覆盖从托管-本机模式切换的开销来获得本机代码的速度增益(并且本机代码可以通过asm注入来提高性能)。

3
使用try-catch块的最佳实践是什么?根本不使用它们——您可以列举大约3个例子,在这些例子中,您确实应该使用try-catch。这对于寻找简单答案的新手来说可能过于复杂了...(简单答案是:有时候,绝对需要try-catch)。我会惊讶地发现甚至是最简单的项目也没有使用try-catch...在我看来,这一定不是做一些非常困难或有趣的事情。 - Don Cheadle
谢谢您提供这样的答案,因为它让我们更深入地了解计算机内部发生的情况。为此点赞。 - Mikhail T.
如果发生了什么事情,那么如何向用户显示自定义消息呢?比方说,在某个类库的后端发生了什么。你会如何将该消息“抛出”到表示层呢? - Arie

6

性能在这两种情况下可能是相同的(但如果您想确保,可以运行一些测试)。异常检查仍然在每次循环时发生,只是在捕获后跳到其他位置。

然而,行为不同。

在第一个例子中,一个项目出现错误会被捕获,循环将继续执行其余项目。

在第二种情况下,一旦出现错误,剩余的循环就永远不会执行。


0

这取决于你想要实现什么 :) 第一个将在循环中尝试每个函数,即使其中一个失败,其余的也会运行...第二个将在出现错误时中止整个循环...


0
在第一个例子中,循环会在捕获异常后继续执行(除非你告诉它停止)。也许你想在需要收集错误数据到 List (在 catch 中进行的操作)并在最后发送电子邮件给某人的情况下使用它,如果出现问题,你不想停止整个过程。
在第二个例子中,如果你希望在发生问题时立即停止以便分析,它将阻止其余的循环发生。

0
你应该更关注所需行为而不是性能。考虑是否要在异常发生时继续循环的能力,以及你将在何处进行任何清理。在某些情况下,您可能希望在循环内部和外部都捕获异常。 try...catch 对性能影响较小。实际上可能导致异常的大多数操作,其引起异常所需的时间长得多,因此在大多数情况下,性能差异可以忽略不计。
在有任何可测量的性能差异的情况下,你会在循环内做很少的工作。通常在这种情况下,你仍然希望在循环外使用 try...catch,因为在循环内没有任何需要清理的内容。

0

对于大多数应用程序来说,性能差异将是可以忽略的。

但问题是,如果其中一个失败,是否要继续处理循环中的其余项。如果是,请使用内部foreach,否则请使用单个外部循环。


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