反转IF语句


39

因此,我已经编程了几年,最近又开始更多地使用ReSharper。ReSharper经常向我建议的一件事是“反转'if'语句以减少嵌套”。

假设我有以下代码:

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
    }
}

ReSharper建议我这样做:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

看来,无论进行多少嵌套,ReSharper都会总是建议我反转IF。这个问题是我至少在某些情况下喜欢嵌套。对我来说,似乎更容易阅读和弄清某些地方的情况。并非总是如此,但有时我感到更舒适的嵌套。

我的问题是:除了个人喜好或可读性之外,还有其他减少嵌套的理由吗?是否存在性能差异或其他我可能不知道的东西?



24
这里的关键是“ Resharper建议”。请看一下建议,如果它使代码更易于阅读且一致,请使用它。对于每个循环,它执行相同的操作。
乔恩·雷诺

通常,我会降级那些尖锐的提示,但我并不总是遵循这些提示,因此它们不会再出现在侧栏中。
CodesInChaos

1
ReSharper消除了负面影响,这通常是一件好事。但是,如果该块确实像您的示例一样简单,则它没有建议在此很好地适合的三元条件
1998年

2
ReSharper也不建议将条件转移到查询中吗?foreach(someObjectList.Where(object => object!= null)中的someObject){...}
Roman Reiner

Answers:


63

这取决于。在您的示例中,具有非反向if语句不是问题,因为的主体if非常短。但是,通常您希望在主体增长时反转语句。

我们只是人类,很难一次掌握多件事情。

当一个语句的主体很大时,反转该语句不仅可以减少嵌套,而且还告诉开发人员他们可以忘记该语句上方的所有内容,而只关注其余部分。您很可能会使用反向语句尽快消除错误的值。一旦知道这些都不在游戏中,剩下的就是可以实际处理的值。


64
我喜欢看到开发人员意见的潮流。代码中有太多嵌套,仅是因为存在一种非常流行的妄想,即breakcontinue并且return没有构造多个结构。
JimmyJames

6
问题是中断等仍然是您必须牢记的控制流,“这不能为x,否则它将退出循环的顶部”,如果它的null检查仍然会引发异常,则可能无需进一步考虑。但是当它的细微差别“在星期四只为该状态运行此代码的一半”时,它就不太好了
Ewan

2
@Ewan:“这不能是x或它将退出循环顶部”和“这不能是x或它不会进入该块”之间有什么区别?
科林

重要的x的复杂性和含义不重要
Ewan

4
@Ewan:我觉得break/ continue/ return有超过一个真正的优势else块。提早退房,有两个街区:退房街区和停留在那里街区;与else3块:如果,否则,不管涉及后(这是用来无论采取什么样的分支)。我更喜欢少块。
Matthieu M.

33

不要避免负面影响。即使CPU喜欢它们,人类也会像解析它们一样吮吸。

但是,尊重成语。我们人类是习惯性的动物,因此我们更喜欢穿着破旧的道路来开满杂草的道路。

foo != null可悲的是,它已成为一种习语。我几乎没有注意到这些单独的部分。我看了一下,然后想,“哦,现在可以安全点了foo”。

如果您想推翻那个(哦,有一天,请),您需要一个非常有力的理由。您需要使我的眼睛毫不费力地滑下来并立即理解它的东西。

但是,您给我们的是:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

在结构上甚至都不一样!

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    if(someGlobalObject == null) continue;
    someSideEffectObject = someGlobalObject.SomeProperty;
}

那不是我懒惰的大脑所期望的那样。我以为我可以继续添加独立代码,但我不能。这使我考虑了我现在不想考虑的事情。你打破了我的习惯,但没有给我更好的习惯。全部是因为您想保护我免受那消极的消极情绪的伤害,这是一个令人讨厌的小小的自我。

很抱歉,也许ReSharper在90%的时间里都建议这样做,但是在空检查中,我认为您发现了一个极端的情况,最好忽略它。工具根本不擅长教您什么是可读代码。问一个人。做同行评审。但是不要盲目地使用该工具。


1
好吧,如果循环中有更多代码,则它的工作方式取决于所有内容的组合方式。它不是独立的代码。问题中的重构代码对于该示例是正确的,但如果不是孤立地,则可能不正确-这是您要去的地方吗?(不过,+ 1还是请避免产生负面影响)
Baldrickk

@Baldrickk if (foo != null){ foo.something() }让我继续添加代码而无需关心是否foo为null。这不是。即使现在我不需要这样做,我也没有动力说“好吧,如果需要的话,他们可以重构它”,从而搞砸了未来。FEH。软件只有在易于更改的情况下才是软的。
candied_orange

4
“不加关心地保持添加代码”要么代码应该相关(否则,它可能应该是分开的),或者应该小心理解要修改的功能/块的功能,因为添加任何代码都可能改变行为超出了预期。对于像这样的简单情况,我认为这都不重要。对于更复杂的情况,我希望理解代码会具有先机,因此我将选择我认为最清楚的那个样式。
Baldrickk

相关和交织在一起不是同一回事。
candied_orange

1
不要回避负面情绪:D
VitalyB

20

关于中断是否比嵌套更糟糕是一个古老的争论。

人们似乎接受参数检查的尽早返回,但是在大部分方法中它都被拒绝了。

就个人而言,即使这意味着更多的嵌套,我也避免继续,中断,跳转等。但是在大多数情况下,您都可以避免两者。

foreach (someObject in someObjectList.where(i=>i != null))
{
    someOtherObject = someObject.SomeProperty;
}

显然,当您有许多层时,嵌套的确会使事情变得难以处理

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
            }
        }
    }
}

最糟糕的是

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
                return;
            }
            continue;
        }
        someObject.z --;
        if(myFunctionWithSideEffects(someObject) > 6) break;
    }
}

11
喜欢这一行:foreach (subObject in someObjectList.where(i=>i != null))不知道为什么我从未想到过这样的事情。
巴里·富兰克林

@BarryFranklin ReSharper在foreach上应带有绿色虚线下划线,并带有“转换为LINQ表达式”作为建议。根据操作,它可能还会执行其他linq方法,例如Sum或Max。
凯文·费

@BarryFranklin这可能是一个建议,您希望将其设置为较低的水平,并且只能谨慎使用。尽管它对于像本例这样的琐碎情况很好用,但我发现在更复杂的代码中,它可以将更复杂的条件部分分成可被Linqifiable的位,而其余主体倾向于创建比原始情况更难理解的东西。通常,我至少会尝试一下建议,因为当它可以将整个循环转换为Linq时,结果通常是合理的。但是一半的结果往往会使快速的IMO变得难看。
Dan Neely

具有讽刺意味的是,resharper的编辑具有完全相同的嵌套级别,因为它还带有if(如果有)且后跟一线。
彼得

嘿,虽然没有括号!这显然是允许的:/
Ewan

6

的问题continue在于,如果在代码中添加更多行,则逻辑会变得很复杂,并且可能包含错误。例如

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    ...  imagine that there is some code here ...

    // added later by a Junior Developer
    doSomeOtherFunction(someObject);
}

你要拨打doSomeOtherFunction()所有 someObject,或者只在非空?使用原始代码,您可以选择,而且更清晰。随着continue人们可能忘了:“哦,是的,回到那里,我们跳过空”,你甚至不必调用它的所有someObjects的选项,除非你把该呼叫在方法的开始可能是不可能的或不正确由于其他原因。

是的,很多嵌套很难看,而且可能造成混乱,但是在这种情况下,我将使用传统样式。

额外的问题: 为什么列表中以空开头?最好不要从头开始添加null,在这种情况下,处理逻辑要简单得多。


12
循环中应该没有足够的代码,以至于没有滚动就无法在其顶部看到空检查。
Bryan Boettcher

@Bryan Boettcher同意,但并非所有代码都写得很好。甚至几行复杂的代码都可能使人们忘记(或误导)继续。在实践中我确定与return的事业,他们形成了“划清界限”,但国际海事组织breakcontinue导致混乱,并且在大多数情况下,都最好避免。
user949300

4
@ user949300经过十多年的发展,我从未发现破裂或继续引起混乱。我让他们陷入混乱,但他们从来都不是原因。通常,对开发人员的错误决定是造成这种情况的原因,无论使用哪种武器,都可能引起混乱。
corsiKa '17

1
@corsiKa 苹果SSL错误实际上是goto错误,但很可能是中断或继续错误。但是,代码太可怕了……
user949300 '17

3
@BryanBoettcher在我看来,问题似乎是认为继续或多次返回是可以的相同开发人员,也认为将它们埋入大型方法主体中也是可以的。因此,我对连续/多次收益的每一次体验都陷入了混乱的代码之中。
安迪

3

这个想法是早日回归。早return在一个循环变得早continue

这个问题有很多好的答案:我应该早点从函数返回还是使用if语句?

请注意,虽然该问题是基于意见而关闭的,但430票以上赞成提前归还,〜50票是“取决于”,而8票反对早期归还。因此,有一个非常强烈的共识(这或者说我不好算,这是合理的)。

就个人而言,如果要使循环主体早日继续并且需要足够长的时间(3-4行),那么我倾向于将循环主体提取到一个函数中,在该函数中我使用早期返回而不是早期继续。


2

当满足您的方法的主要条件时,此技术对于验证前提条件很有用。

我们经常颠倒ifs我的工作,并雇用其他人。过去经常在审查PR时经常指出我的同事如何删除三个嵌套层!由于我们确实推出了ifs,因此发生的频率降低了。

这是一个可以推广的玩具示例

function foobar(x, y, z) {
    if (x > 0) {
        if (z != null && isGamma(y)) {
            // ~10 lines of code
            // return something
        }
        else {
           return y
        }
    }
    else {
      return y + z
    }
}

像这样的代码,在一种情况下(其余的只是返回值或小的语句块)很常见。将上面的内容重写为

function foobar(x, y, z) {
    if (x <=0) {
        return y + z
    }
    if (z == null || !isGamma(y) {
       return y
    }
    // ~ 10 lines of code
    // return something
}

在这里,我们的重要代码(不包括10行)在函数中没有三重缩进。如果您使用的是第一种样式,则很容易将长块重构为一种方法或允许缩进。这是节省的地方。在一个地方,这是微不足道的节省,但是在许多类的许多方法中,您无需生成额外的功能来使代码更简洁,而不必再进行多层次的丑陋处理压痕


在回应OP的代码示例时,我确实同意那是一个急切的崩溃。特别是由于我使用的样式为1TB,因此反转会导致代码增大。


0

转载建议通常是有用的,但应始终进行严格评估。它寻找一些预定义的代码模式并提出转换建议,但是它不能考虑使代码对人类可读性更高的所有因素。

在所有其他条件相同的情况下,最好使用较少的嵌套,这就是ReSharper将建议减少嵌套的转换的原因。但是所有其他条件都不相等:在这种情况下,转换需要引入continue,这意味着生成的代码实际上更难遵循。

某些情况下,一个交换嵌套的水平continue 可能确实是一种进步,但是这取决于有问题的代码,这是超越ReSharpers机械规则的理解的语义。

在您的情况下,ReSharper的建议会使代码更糟。因此,请忽略它。或更好:不要使用建议的转换,但要对如何改进代码进行一些思考。请注意,您可能多次分配了相同的变量,但是只有最后一次分配才有效,因为前一次分配只会被覆盖。这看起来确实像个错误。ReSharpers的建议不能解决该错误,但是确实使一些可疑的逻辑正在变得更加明显。


0

我认为,这里更大的一点是,循环的目的决定了在循环内组织流程的最佳方式。如果要在循环浏览其余元素之前先过滤掉某些元素,则continue尽早删除是有意义的。但是,如果您要在一个循环中执行不同类型的处理,则使之if真正明显可能更有意义,例如:

for(obj in objectList) {
    if(obj == null) continue;
    if(obj.getColour().equals(Color.BLUE)) {
        // Handle blue objects
    } else {
        // Handle non-blue objects
    }
}

请注意,在Java Streams或其他功能惯用语中,可以使用以下方法将过滤与循环分离:

items.stream()
    .filter(i -> (i != null))
    .filter(i -> i.getColour().equals(Color.BLUE))
    .forEach(i -> {
        // Process blue objects
    });

顺便说一句,这是我喜欢Perl的if(unless)倒置到单个语句的原因之一,它允许以下类型的代码,我觉得这很容易阅读:

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
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.