try-catch-finally块中无关的代码“有多糟糕”?


11

这是一个相关的问题: 在返回不良样式/危险之后,使用finally子句进行工作吗?

在引用的Q中,最终代码与所使用的结构和预取的必要性有关。我的问题有所不同,我认为这与广大受众密切相关。我的特定示例是C#winform应用程序,但这同样适用于C ++ / Java。

我注意到很多try-catch-finally块,其中有很多与异常无关的代码以及埋在该块中的异常处理/清除。而且,我会偏向于使用非常紧密的try-catch-finally块,并将其代码与异常和处理紧密相关。这是我所看到的一些示例。

尝试块将设置许多初步调用和变量,从而导致可能引发的代码。日志信息将被设置并在try块中运行。

最后,块将具有表单/模块/控件格式化调用(尽管应用程序即将终止,如catch块所示),以及创建新对象(例如面板)。

大致:

    methodName(...)
    {
        尝试
        {
            //该方法的大量代码...
            //可能抛出的代码...
            //该方法还有更多代码和返回值...
        }
        抓住(某物)
        {//处理异常}
        最后
        {
            //由于异常而进行一些清理,将事情关闭
            //有关所创建内容的更多代码(忽略任何异常都可能引发)...
            //可能会创建更多对象
        }
    }

该代码有效,因此具有一定的价值。它没有很好地封装,逻辑有点混乱。我(痛苦地)熟悉了代码移位和重构的风险,因此我的问题归结为想要了解其他人对类似结构的代码的经验。

不良的风格是否有理由进行更改?有没有人被类似的情况严重烧死?您是否愿意分享糟糕经历的细节?别说是因为我反应过度,风格还不错吗?获得整理事物的维护利益?


“ C ++”标记不属于此处,因为C ++没有(也不需要)finally。RAII / RRID / SBRM(您喜欢的首字母缩写)都涵盖了所有良好的用途。
David Thornley,2012年

2
@DavidThornley:除了使用“ finally”关键字的示例外,其余问题完全适用于C ++。我是C ++开发人员,该关键字并没有真正使我感到困惑。并考虑到我有我自己的球队上半定期对话类似,这个问题是非常相关的是我们使用C ++做
地塞米松

@DXM:我很难想象这一点(而且我确实知道finallyC#的作用)。什么是C ++等效项?我在想的是catchC之后的代码,它对C#之后的代码也是如此finally
David Thornley,2012年

@DavidThornley:最后的代码无论如何执行的代码。
orlp 2012年

1
@nightcracker,那是不正确的。如果执行,它将不会执行Environment.FailFast();如果您有未捕获的异常,则可能无法执行。如果您有一个finally手动迭代的迭代器块,它将变得更加复杂。
svick

Answers:


10

当我不得不处理开发人员编写的可怕的旧版Windows Forms代码时,我经历了非常相似的情况,这些代码显然不知道他们在做什么。

首先,您没有反应过度。这是错误的代码。就像您说的那样,catch块应该涉及中止并准备停止。现在不是创建对象(特别是面板)的时候。我什至无法开始解释为什么这很糟糕。

话虽如此...

我的第一个建议是:如果没有损坏,请勿触摸!

如果您的工作是维护代码,则必须尽最大努力不破坏它。我知道这很痛苦(我去过那里),但是您必须尽最大努力不破坏已经起作用的内容。

我的第二个建议是:如果必须添加更多功能,请尝试尽可能保留现有的代码结构,以免破坏代码。

例如:如果有一个令人毛骨悚然的switch-case语句,使您感到可以被适当的继承所代替,那么在决定开始四处移动之前,必须小心谨慎并三思而后行。

您肯定会发现重构是正确的方法,但要当心的情况是:重构代码更有可能引入bug。您必须从应用程序所有者的角度而不是从开发人员的角度做出决定。因此,您必须考虑解决问题所需的精力(金钱)是否值得重构。我见过很多次开发人员花几天时间来修复一些并没有真正因为他认为“代码丑陋”而真正损坏的东西。

我的第三条建议是:如果您破坏了代码,您会被烫死,这是否是您的错并不重要。

如果您被雇用来进行维护,那么应用程序是否崩溃就无关紧要了,因为其他人做出了错误的决定。从用户的角度来看,它以前一直在工作,现在您将其破坏了。把它弄坏了!

Joel在他的文章中很好地解释了为什么不应该重写遗留代码的几个原因。

http://www.joelonsoftware.com/articles/fog0000000069.html

因此,您应该对这种代码感到非常难过(并且绝对不要编写类似的代码),但是维护它却是完全不同的怪物。

关于我的经验:我不得不将代码维护大约一年,最终我能够从头开始重写它,但不能一次全部重写。

发生的事情是代码太糟糕了,无法实现新功能。现有应用程序存在严重的性能和可用性问题。最终,我被要求进行更改,这将花费我3-4个月的时间(主要是因为使用该代码比平时花费了更多时间)。我以为我可以在大约5到6个月内重写整个代码(包括实现所需的新功能)。我将此提议带给了利益相关者,他们同意重写它(对我来说是幸运的)。

在我改写了这篇文章之后,他们知道我可以提供比他们现有的更好的东西。因此,我能够重写整个应用程序。

我的方法是一步一步地重写它。首先,我替换了整个UI(Windows窗体),然后开始替换了通信层(Web服务调用),最后我替换了整个Server实现(这是一种胖客户端/服务器类型的应用程序)。

几年后,此应用程序已变成整个公司使用的漂亮的关键工具。我100%确信,如果不重写整个内容,那将永远不可能。

即使我能够做到这一点,但重要的部分是利益相关者批准了它,而且我能够说服他们值得花钱。因此,在您必须维护现有应用程序的同时,尽最大努力不要破坏任何内容,并且如果您能够说服所有者重新编写它的好处,请尝试像杰克开膛手一样:按部就班。


1
+1。但是最后一句话是指连环杀手。那真的有必要吗?
MarkJ 2012年

我喜欢其中的一部分,但与此同时将残缺的设计留在原处,添加内容通常会导致设计变差。尽管采取了一种谨慎的方法,但最好弄清楚如何修复和修复它。
里奇·克拉克森

@RickyClarkson我同意。很难知道什么时候过度使用(实际上没有必要进行重构)以及何时通过添加更改来损害应用程序。
亚历克斯

@RickyClarkson我非常同意,以至于我什至可以重写我参与的那个项目。但是很长一段时间以来,我不得不小心翼翼地添加一些东西,以尽量减少损坏。除非请求是要修复导致错误的主要设计决策的东西(通常不是这种情况),否则我应该说应用程序的设计不应该被触及。
亚历克斯

@RickyClarkson这是我想参与的社区经验的一部分。将所有旧版代码都声明为不良代码是一种同样糟糕的反模式。但是,这段代码中我正在处理的某些事情实际上不应该作为finally块的一部分来完成。亚历克斯(Alex)的反馈和回应是我一直在阅读的内容。

2

如果不需要更改代码,请保持原样。但是,如果由于必须修复某些错误或更改某些功能而必须更改代码,则无论是否愿意,都必须对其进行更改。恕我直言,如果您首先将其重构为更小,更易理解的片段,然后添加功能,则它通常更安全且更不易出错。当您拥有自动重构工具时,这可以相对安全地完成,引入新错误的风险很小。而且我建议您应该得到一本迈克尔·费瑟斯的书

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

这将为您提供有价值的提示,说明如何使代码更具可测试性,添加单元测试以及在添加新功能时避免破坏代码。

如果操作正确,则您的方法将有望变得足够简单,try-catch-finally将不再在错误的位置包含任何不相关的代码。


2

假设您有六个要调用的方法,只有第一个没有错误完成时,才应调用其中四个。考虑两种选择:

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

在第二个代码中,您将处理返回代码之类的异常。从概念上讲,这等同于:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

如果我们要在try / catch块中包装所有可能引发异常的方法,那么在语言功能中完全具有异常的意义是什么?没有好处,而且打字更多。

首先,我们在语言中存在异常的原因是,在糟糕的返回码时代里,我们经常遇到以上情况,在该情况下,我们有多个可能返回错误的方法,每个错误都意味着完全放弃所有方法。在我职业生涯的开始,在过去的C时代,我到处都看到这样的代码:

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

这是一种非常常见的模式,通过让您回到上面的第一个示例,可以非常巧妙地解决异常。说每个方法都有自己的异常块的样式规则将其完全排除在窗口之外。那为什么还要打扰呢?

您应该始终努力使try块替代概念上作为一个单元的代码集。它应该包围代码,如果代码单元中有任何错误,那么在代码单元中运行任何其他代码毫无意义。

回到异常的原始目的:请记住,它们是由于异常的代码通常无法在实际发生时轻松地对其进行处理而创建的。异常的要点是使您可以在代码的后期显着地处理错误,或使堆栈更进一步。人们会忘记这一点,并且在许多情况下,当他们最好只是简单地记录下所讨论的方法会引发此异常时,就会在本地捕获它们。世界上有太多这样的代码:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

在这里,您只是添加图层,而图层会增加混乱。只是这样做:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

除此之外,我的感觉是,如果您遵循该规则,并且发现自己在该方法中遇到了多个try / catch块,那么您应该质疑该方法本身是否太复杂,是否需要分解为多个方法。

要点:try块应包含一组代码,如果该块中的任何地方发生错误,则应中止该组代码。这组代码是一行还是一千行无关紧要。(尽管如果一个方法中有一千行,那么您还会遇到其他问题。)


史蒂文(Steven)-感谢您提出的合理安排,我完全同意您的想法。我住在单个try块中的合理相关或顺序类型的代码没有问题。如果能够发布实际的代码段,那么在第一个可抛出调用之前,它会显示5-10个完全不相关的调用。一个特定的finally块要糟糕得多-拆分了几个新线程,并调用了其他非清理例程。就此而言,finally块中的所有调用与可能引发的任何事件都不相关。

这种方法的一个主要问题是,它在method0可能准预期会引发异常的情况与未引发异常的情况之间没有区别。例如,假设method1有时可能会抛出一个InvalidArgumentException,但如果没有抛出,它将把一个对象置于一个暂时无效的状态,该状态将通过进行纠正method2,这有望使该对象始终回到一个有效状态。如果method2抛出一个InvalidArgumentException,则期望捕获此类异常的代码将捕获该异常method1,即使...
supercat

...系统状态将符合代码的期望。如果从方法1引发的异常应与从方法2引发的异常的处理方式不同,那么如何在不包装它们的情况下明智地实现该异常?
2014年

理想情况下,您的例外情况应足够详细,以至于这种情况很少见。
弄乱了机器人
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.