支持或反对使用Try / Catch作为逻辑运算符的论点


36

我刚刚在我们公司的应用程序中发现了一些可爱的代码,这些代码使用Try-Catch块作为逻辑运算符。
意思是,“执行一些代码,如果抛出此错误,则执行此代码,但如果抛出此错误,则执行此第三项操作”。
它使用“最后”作为出现的“其他”语句。
我知道这本质上是错的,但是在我去打架之前,我希望能有一些深思熟虑的论点。
嘿,如果您有关于以这种方式使用Try-Catch的参数,请务必告知。

对于任何想知道的人,该语言是C#,所讨论的代码大约有30多行,并且正在寻找特定的异常,因此它无法处理所有异常。


13
我只反对这种做法,并且因为您似乎已经确信这是个坏主意,所以我不再打扰他们了。
FrustratedWithFormsDesigner

15
@FrustratedWIthFormsDesigner:这是错误的推理。不相信的人呢?关于我的具体问题,我具体问原因是什么,因为我不能仅凭自己知道错就告诉“为什么”。
James P. Wright

3
我的意见高度取决于所讨论的代码的实际作用。有些事情是无法事先检查的(或者在尝试时会出现竞争条件,例如许多文件操作-在检查和操作之间的延迟中,文件可能会发生任何事情),必须进行检查try。并非所有需要例外的例外情况在此特定情况下都必定是致命的。因此,您是否可以在不使用异常的情况下,以更简单,同等或更可靠的方式进行操作?

11
我希望他们不要真的将“ finally”块用作“ else”,因为“ finally”块总是在前面的代码之后运行,而不管抛出任何异常。
jimreed 2011年

2
他们使用什么语言?例如,在OCaml中,这很好,标准库会例行抛出异常。那里的异常处理非常便宜。但是在CLI或JVM中效率不高。
SK-logic

Answers:


50

异常处理往往是处理流控制的昂贵方法(对于C#和Java肯定如此)。

构造异常对象时,运行时会做很多工作-将堆栈跟踪在一起,弄清楚异常的处理位置等等。

如果将流控制语句用于流控制,则无需扩展所有这些内存和CPU资源成本。

此外,还有一个语义问题。例外是针对特殊情况,而不是正常的流量控制。应该使用异常处理来处理意外的/异常情况,而不是像正常的程序流程那样处理,因为否则,未捕获的异常将告诉您更少的信息。

除了这两个以外,还有其他人在阅读代码。以这种方式使用异常并不是大多数程序员所期望的,因此会影响可读性和代码的可理解性。当人们看到“异常”时,就会想到-一件不好的事情发生了,不应该正常发生的事情。因此,以这种方式使用异常只会造成混乱。


1
关于性能的一个好地方,尽管这也取决于平台的具体情况。
FrustratedWithFormsDesigner

4
如果那是唯一的缺点,那么谢谢-如果它为我提供更好的代码,我会放弃性能(除非在性能关键的路径中,否则;但是幸运的是,即使在一般性能的应用程序中,它也仅占所有代码的20% -危急)。

5
@delnan-不,不是唯一的缺点。
Oded

2
除了“不客气”一词外,我同意您的帖子。在代码中有时会发生异常。它们中的大多数应该是可预测的,例如与数据库或服务调用的连接失败,或者在您处理非托管代码时。您发现无法控制的问题并加以解决。但是我同意您永远都不应基于可以在代码中避免的异常进行流控制。
SoylentGray 2011年

4
“异常处理往往是处理流控制的昂贵方法”。对于Python是错误的。
美国洛特

17

我刚刚在我们公司的应用程序中发现了一些可爱的代码,这些代码使用Try-Catch块作为逻辑运算符。意思是,“执行一些代码,如果抛出此错误,则执行此代码,但是如果抛出此错误,则执行此第三项操作”。它使用“最后”作为出现的“其他”语句。我知道这本质上是错误的...

你怎么知道?我放弃了所有这些“知识”,现在只是相信最简单的代码是最好的。假设您想将字符串转换为Optional,如果解析失败,该字符串为空。没错:

try { 
    return Optional.of(Long.valueOf(s)); 
} catch (NumberFormatException) { 
    return Optional.empty(); 
}

我完全不同意“例外是针对特殊条件”的通常解释。当函数无法返回可用值或方法不能满足其后置条件时,将引发异常。在出现明显的性能问题之前,多长时间抛出一次异常都无关紧要。

异常通过允许将错误处理与正常流程分开来简化了代码。只需编写尽可能简单的代码,如果使用try-catch或引发异常更容易,则可以这样做。

异常通过减少代码的路径数来简化测试。没有分支的函数将完成或引发异常。具有多个if语句以检查错误代码的函数具有许多可能的路径。错误条件之一很容易弄错,或者完全忘记一个条件很容易,因此某些错误条件会被忽略。


4
我认为这实际上是一个很好的论据。您的代码简洁明了,并且在做什么方面有意义。这实际上类似于我所看到的代码中发生的事情,只是它更加复杂,嵌套并且跨越了30多行代码。
詹姆斯·P·赖特

@James:如果您提取一些较小的方法,听起来像代码会很好。
凯文·克莱恩

1
有人可能会说Java确实可以使用C#的TryParse之类的东西。在C#中,该代码为int i; if(long.TryParse(s, out i)) return i; else return null;。如果无法解析该字符串,则TryParse返回false。如果可以解析,则将输出参数设置为解析结果并返回true。没有异常发生(即使在TryParse内部也没有异常),尤其是没有任何表示程序员错误的类型异常,如.NETs FormatException总是表明。
凯文·卡斯卡特

1
@Kevin Cathcart,那为什么更好呢?对我来说,捕获NumberFormatException的代码对我而言更加明显,然后检查TryParse的结果是否为null。
温斯顿·埃韦特

1
@ChrisMiskowiec,我怀疑您发现更清晰的哪个与您过去的习惯差不多。我发现捕获异常要容易得多,然后检查魔术值。但这都是主观的。客观地讲,我认为我们应该更喜欢例外,因为它们具有合理的默认值(使程序崩溃),而不是具有隐藏错误的默认值(继续进行,就好像什么都没发生一样)。的确,.NET的例外情况表现不佳。但这是.NET设计的结果,而不是异常的性质。如果在.NET上,是的,您必须这样做。但是原则上,例外更好。
温斯顿·埃韦特

12

当使用异常执行控制流时,调试和维护工作非常困难。

从本质上讲,异常被设计为一种用于更改程序的正常控制流的机制-执行异常活动,引起异常副作用,以此作为摆脱特别紧密的绑定的方法,而这种绑定无法用较简单的方式处理手段。例外是例外。这意味着,根据您正在使用的特定环境,对常规控制流使用异常可能导致:

  • 效率低下环境为了安全地执行异常所需的相对困难的上下文更改而必须跳过环境的额外障碍,这需要工具和资源。

  • 调试困难有时有用(在尝试调试程序时),当发生异常时,信息会丢出窗口。您可能会丢失与了解运行时行为有关的程序状态或历史记录

  • 维护问题执行流程很难通过异常跳转来跟踪。除此之外,黑匣子类型代码内部可能会引发异常,而引发异常时,黑匣子类型代码的行为可能不容易理解。

  • 糟糕的设计决策用这种方式构建的程序会激发一种思路,在大多数情况下,这些思路很难轻松地映射到优雅地解决问题的过程。最终程序的复杂性使程序员无法充分理解其执行,并鼓励做出决定,从而导致短期改进,而长期成本却很高。


10

我已经看到这种模式使用了几次。

有两个主要问题:

  • 这是非常昂贵的(实例化异常对象,收集调用堆栈等)。某些编译器实际上可能能够对其进行优化,但是在这种情况下,我不会指望这一点,因为异常不是用于此类用途的,因此您不能指望人们为此进行优化。
  • 使用异常进行控制流实际上是一个跳跃,很像goto。出于多种原因,跳跃被认为是有害的。如果所有替代方案都有明显的缺点,则应使用它们。实际上,在我所有的代码中,我只记得2种情况,其中跳转显然是最好的解决方案。

2
只是把它放在那里,如果/否则也被认为是跳跃。区别在于,这只是一次跳转,只能在该特定位置发生(并且阅读起来容易得多),而且您不必担心标签的名称(也不必尝试/捕获) ,但与goto相比)。但是,只要说“这是一个跳跃,那是不好的”,还说如果/否则不好,那不是。
jsternberg 2011年

1
@jsternbarg:是的,如果/ else实际上已经编译为在机器级别跳转,但是在语言级别,跳转目标是下一个语句,而在循环的情况下,它是当前语句。我会说,这是一个更大的步骤,而不是一个跳跃;)
back2dos

9

有时例外最快。我见过这样的情况:即使在Java中,空对象异常也比使用控制结构要快(我目前无法引用该研究,因此您必须信任我)。当Java必须花时间并填充自定义异常类的堆栈跟踪而不是使用本机异常类(似乎至少已部分缓存)时,就会出现问题。在说某些东西单方面更快或更慢之前,最好先进行基准测试。

在Python中,这样做不仅速度更快,而且做一些可能导致异常然后处理该错误的方法更为正确。是的,您可以强制执行类型系统,但是这与语言的原理背道而驰-相反,您应该简单地尝试调用方法并捕获结果!(测试文件是否可写是相似的-只需尝试写入文件并捕获错误)。

我见过一些时候,做一些愚蠢的事情(例如查询表)比找出一个表是否存在于PHP + MySQL中更快(我对它进行基准测试的问题实际上是我唯一接受的否定表决答案)。 。

综上所述,应出于以下几个原因限制例外的使用:

  1. 误吞嵌套异常。这很重要。如果您发现某个嵌套嵌套的异常,而其他人正在尝试处理该异常,那么您就是在与其他程序员(甚至您!)开枪。
  2. 测试变得不明显。一个代码块中有一个异常,可能有几处错误。另一方面,布尔值虽然理论上可能很烦人,但通常不是。这一点尤其正确,因为try...catch控制流的依附者通常(以我的经验)不遵循“尽量减少try块中的代码”的理念。
  3. 它不允许有else if块。够用了(如果有人反对“但是他们可以使用不同的异常类”,我的回答是“去你的房间,除非你考虑了你所说的,否则不要出来。”)
  4. 这在语法上是令人误解的。 Exception对于世界其他地区(不是对try...catch控制流哲学的拥护者),则意味着某事物已进入不稳定(尽管可能可恢复)状态。不稳定的状态是坏的,如果我们实际上有可以避免的例外情况,它应该使我们整夜都处于睡眠状态(它实际上使我无所适从,没有说谎)。
  5. 它不符合常见的编码风格。我们的工作,无论是使用代码还是使用UI,都是为了使世界变得尽可能明显。try...catch控制流违反了通常认为的最佳实践。这意味着新来的人需要花费更多的时间来学习该项目,这意味着工时数的增加绝对没有任何收益。
  6. 这通常会导致代码重复。最终块虽然不是严格必需的,但需要解决被中断的try块保持打开状态的所有悬空指针。因此,请尝试块。这意味着您可以拥有一个try{ obj.openSomething(); /*something which causes exception*/ obj.doStuff(); obj.closeSomething();}catch(Exception e){obj.closeSomething();}。在更传统的if...else情况下,closeSomething()复制和粘贴工作的可能性较小(再次是个人经验)。(不可否认,这个特定的争论与我遇到的人更多的是关系,而不是实际的哲学本身)。

AFAIK#6仅在Java中是一个问题,与其他现代语言不同,Java没有闭包或基于堆栈的资源管理。这当然不是使用异常所固有的问题。
凯文·克莱恩

4和5对我来说似乎是同一点。如果您的语言是python,则3-6不适用。1和2是潜在的问题,但我认为可以通过最小化try块中的代码来处理它们。
Winston Ewert

1
@温斯顿我不同意。1和2是主要问题,尽管被更好的编码标准所削弱,但仍然使人感到疑惑的是,简单地避免开始时可能出现的错误是否会更好。如果您的语言是Python,则3适用。4-5也可以申请Python,但不是必须的。4和5是非常不同的点-误导与风格上的差异不同。误导性代码可能正在执行某些操作,例如将触发器命名为“停止按钮”以启动车辆。文体上,在另一方面,它也可以类似于避免C.所有块缩进
cwallenpoole

3)Python有一个else异常块。这是为了避免大量的问题有所帮助你通常会得到1和2
温斯顿·尤尔特

1
明确一点,我不主张使用异常是您的一般流控制机制。我主张对任何一种“失败”模式都抛出一个异常,无论它多么微小。我认为,与返回值检查版本相比,异常处理版本更干净并且更不可能隐藏错误。理想情况下,我希望语言使捕获嵌套错误更加困难。
温斯顿·埃韦特

4

我的主要观点是使用try / catch进行逻辑操作会破坏逻辑流程。通过非逻辑结构遵循“逻辑”(对我而言)是违反直觉和令人困惑的。我习惯于将我的逻辑读为“如果条件则A否则B”。阅读与“尝试A捕获然后执行B”相同的语句感到很奇怪。如果语句A是一个简单的赋值,那将变得更加奇怪,如果它condition为false ,则需要额外的代码来强制执行异常(并且这样做可能if始终需要-statement)。


2

好吧,只是礼仪问题,在您声明“与他们开始争论”之前,我会问“为什么他们在所有这些不同地方都使用异常处理?”

我的意思是,有几种可能性:

  1. 他们没有能力
  2. 他们所做的事情完全有道理,乍一看可能不会出现。
  3. 有时这是一个品味问题,可能会简化一些复杂的程序流程。
  4. 这是过去的遗物,没有人改变过,如果有人这样做,他们会很高兴

...我认为所有这些可能性都是一样的。因此,很好地问他们。


1

Try Catch仅应用于异常处理。更重要的是,especific异常处理。您的try catch应该只捕获预期的异常,否则,它的格式不正确。如果您需要使用catch全部尝试catch,则可能是您做错了什么。

编辑:

原因:您可以辩称,如果将try catch用作条件运算符,那么您将如何考虑REAL异常?


2
OP正在询问原因/参数。您尚未提供任何信息。
Oded

“您可以争辩说,如果将try catch用作条件运算符,那么您将如何考虑REAL异常?” -通过在catch与捕获“非真实”异常的子句不同的子句中捕获那些真实异常?

@delnan-谢谢!您已经证明了我即将提出的观点。如果一个人回答了您刚才说的我和奥德的话,我只会停止说话,因为他没有寻找他只是固执的原因,我不会因为固执而浪费时间。
AJC

您称我为Studdborn,是因为我指出了此处列出的某些论点中的缺陷吗?请注意,对于这里所述的其他观点,我无话可说。我不喜欢使用异常进行流控制(尽管我也不会谴责任何没有细节的内容,因此我不希望通过OP进行详细说明),我只是在扮演魔鬼的拥护者。

1
使用try-catch作为条件运算符绝不会阻止它同时捕获“实际”异常。
Winston Ewert

1

例外是指发生例外情况时发生的情况。该程序是否按常规工作流程正常运行?


1
但为什么?问题的重点是为什么(或为什么不)例外仅对此有用。
温斯顿·埃韦特

并不总是“例外”。例如,在哈希表中找不到键并不是那么特殊,但是OCaml库在这种情况下往往会引发异常。没关系-那里的异常处理非常便宜。
SK-logic

1
-1:重言式
米粉饼干

1

使用异常的原因:

  1. 如果您忘记了遇到特殊情况,您的程序将会终止,并且堆栈跟踪会告诉您原因。如果您忘记为异常情况处理返回值,则不会告诉您程序将在多远的距离出现错误行为。
  2. 仅当存在可以返回的前哨值时,才使用返回值。如果所有可能的返回值都已经有效,那么您该怎么办?
  3. 异常将包含有关所发生事件的更多信息,这可能会有用。

不使用例外的原因:

  1. 许多语言并非旨在快速创建例外。
  2. 遍历堆栈多层的异常可能会在唤醒后留下不一致的状态

到底:

目的是编写传达正在发生的事情的代码。异常可以帮助/阻碍代码的运行。在python字典引用上尝试对KeyError进行try / catch非常完美(只要您知道该语言),对于距离五个函数层相同的KeyError进行try / catch是危险的。


0

在某些情况下,我使用try-> catch作为流控制。如果您的主要逻辑依赖于失败的事物,但是您不想只是抛出异常然后退出……那就是try-> catch块的目的。我写了很多不受监视的服务器端Unix脚本,对于它们来说,失败要比让它们彻底失败更重要。

因此,请尝试使用计划A,如果计划A死了,赶上计划B并运行...如果计划B失败,请最终使用它启动计划C,这将修复A或B或页面中失败的服务之一我。



-1

这是一个坏习惯,但我有时会用python来做。喜欢,而不是检查文件是否存在,我只是尝试将其删除。如果它引发异常,我会继续前进,直到知道它不存在。<==(如果另一个用户拥有该文件,则不一定正确,但是我在用户的主目录中执行此操作,因此应避免这种情况)。

但是,过度使用仍然是一种不好的做法。


3
在该示例中,使用异常而不是“跨越前的眼光”实际上是一个好主意:它避免了竞争情况。检查文件可用性(存在,权限,未锁定)是否成功并不意味着要稍后(即使只是几个操作码)也可以进行访问(打开,删除,移动,锁定等),因为文件可以之间进行更改(删除,移动,更改其权限)。

如果python仅由于文件不存在而引发异常,就象delnan描述的那样,这似乎在鼓励不良习惯。
Per Johansson

@delnan:尽管如此,恕我直言,如果可能遇到这种竞争情况,您应该重新考虑您的设计。在这种情况下,系统中显然缺乏关注点的分离。除非您有充分的理由这样做,否则您应该统一终端的访问权限,或者使用适当的数据库来处理试图对同一持久性数据执行事务的多个客户端。
back2dos

1
这不是一个坏习惯。它是Python中推荐的样式。
Winston Ewert

@ back2dos,在处理外部资源(例如数据库/文件系统的其他用户)时,是否可以阻止它?
温斯顿·埃韦特

-1

我喜欢Apple 定义它的方式:异常仅适用于程序员错误和致命的运行时错误。否则使用错误代码。它帮助我决定何时使用它,对自己说:“这是程序员错误吗?”


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.