C#: 嵌套条件语句 vs continue 语句

20

最近我在使用ReSharper时,它建议我通过反转if条件并使用continue语句来减少某些地方的嵌套。

嵌套条件语句:

foreach(....)
{
    if(SomeCondition)
    {
        //do some things

        if(SomeOtherNestedCondition)
        {
            //do some further things
        }
    }
}

continue 语句:

foreach(....)
{
    if(!SomeCondition) continue;

    //do some things

    if(!SomeOtherNestedCondition) continue;

    //do some further things
}

我理解为什么要减少嵌套以提高性能和内存使用等问题,以及这两个代码片段彼此相等的逻辑。然而,根据我的开发背景,在阅读代码时,before示例更容易理解。

您更喜欢哪种方法?为什么?在日常代码中,您是否使用continue而不是嵌套的if语句?

10个回答

33
作为一种规则,我发现最好始终从包含条件的语句块开始,因为这样可以减少复杂性,更重要的是在它们进一步执行之前就排除了不兼容的情况,这可以提高代码和内存性能。这还可以确保您的条件在维护期间安全,并且不太可能将无效场景传递到不属于它们的代码中。
此外,我认为这两者中的第二个更易读,因为范围层会令人困惑可用什么,很容易在后面的某个层中创建一个变量,但没有意识到它在另一个层中不可用,或者必须进行适当修改来管理它们等问题。
这不仅适用于循环中的continue,还适用于方法的条件应该返回return;而不是让方法开始。
if (valid)
{
    do stuff;
}

它应该始终启动。

if (notValid)
{
    return;
}

2
+1。很好的观点。我支持你的想法。通常我会把所有导致方法返回或退出的条件放在顶部。一般来说,我会先放置异常抛出,然后是if-return块,最后是方法逻辑。 - KP.
1
反对它的论点是它“结构不够严谨”,因为函数有多种结束方式。例如,如果在函数终止之前忘记执行清理代码,可能会遇到麻烦。话虽如此,我比较懒,也认为这不是什么大问题,尤其是在C#中,清理代码的必要性比C++要少。此外,我喜欢我的代码缩进较少。 - Rei Miyasaka
这几乎总是我的论点,所以为了扮演恶魔的代言人,我要提到有时我想在方法退出之前做一些事情(添加日志?),而我经常这样编码可能会让它变得非常麻烦。如果你正在返回一个非错误的有用值,那么我可能会开始尝试使用单个退出点的策略。 - Bill K
@Bill K:在这些事件中,我会编写记录方法,返回我想要在错误范围内返回的内容,然后您只需返回LogAndExit(); 如果需要值,则可以使用通用返回值LogAndExit<SomeReturnType>(defaultRetValue); - Jimmy Hoffa
@Jimmy Hoffa,我说的不是日志记录,而是在最后添加一些功能,就像日志记录一样。例如,在调试期间,输出一行打算删除的日志记录。其他例子可能包括设置标志、增加计数器等。我并不反对你的观点,我经常使用这种模式,只是提到它有时会出现问题。 - Bill K

9

性能方面不应该有显著的差异,这完全是关于可读性。个人认为后者更易于阅读。嵌套较少,更易于阅读。


7

简短回答:

我倾向于使用缩进来暗示某些真正的决策逻辑,即预期有多个可能的执行路径的逻辑。

更长的回答:

通常我喜欢使用缩进块而不是先前的“断点”语句(continuebreakreturnthrow)。

原因:在我看来,断点语句通常会使代码变得更难读。如果你缩进代码,很容易找到某个决策逻辑发生的位置:那将是比其它代码缩进少的第一行。如果你改用带有反转条件的断点语句,你就必须做更多的工作来理解代码分支,并且在哪些情况下跳过某些代码。

对我来说有一个值得注意的例外情况,即在方法开头验证参数。我将该代码格式化如下:

if (thisArgument == null) throw new NullArgumentException("thisArgument");
if (thatArgument < 0) throw new ArgumentOutOfRangeException("thatArgument");
...

原因:由于我不希望这些异常被实际抛出(也就是说,我期望方法被正确调用;在我看来,调用函数负责检测无效输入),所以我不想为不应该发生的事情缩进所有其他代码。


4
使用continue风格的代码,如果有第三个操作与SomeOtherNestedCondition无关,那么如何处理呢?这使得代码的顺序变得重要,我认为这会降低可维护性。
例如:
foreach(....) 
{ 
    if(!SomeCondition) continue; 

    //do some things 

    if(!SomeOtherNestedCondition) continue; 

    //do some further things 

    if(!SomeSecondNonNestedCondition) continue;

    // do more stuff
}

当SomeOtherNestedCondition触发continue;语句时,但仍需要执行SomeSecondNonNestedCondition,会发生什么?
我会重构每个代码块,并使用嵌套的if()调用每个重构后的方法,并保留嵌套结构。

2
代码的顺序在这些过程式语言中总是很重要的。你无法绕过它。 - Matti Virkkunen
在 SomeOtherNestedCondition 之后,我会再添加第三个 continue。如果代码的顺序很重要,那么你最好知道正确的顺序并将代码放在正确的位置...使用嵌套的 if() 既不能省略这一点,也不能使阅读代码时更容易“看到”它...事实上,可以轻松地认为使用 continue 方法使代码片段的顺序更容易看到,因为它们不会隐藏在所有嵌套中... - Charles Bretana
+1 个好点。是的,我意识到必须在适当的地方使用它。有时候,逻辑过于复杂,无法应用 continue 设计。 - KP.
也许我表达得不够清楚,但我认为KP已经抓住了主要观点,即有时候有太多的事情要处理,无法使用“continue”设计。 - Nate
“continue”模式是“&&”运算符的多语句变体。如果需要在逻辑结果已知后继续评估条件,则使用“&”运算符,可能需要在操作数中包装!!()以防它们可能不是0或1。例如,如果(!!(function1()) & !!(function2()))...将始终评估两个函数。 - supercat
显示剩余2条评论

3

结合两者使用。我会在循环顶部使用 if(condition) continue;if(condition) return; 来验证当前状态,然后在下面的嵌套 if 语句中进一步控制我想要完成的任务的流程。


2

简单的回答。哪个更容易阅读和理解就选哪个。


1
原始含义:continue指的是当任何条件为假时,代码将不会被处理。以下是一个例子:
class Program
{
    static bool SomeCondition = false;
    static bool SomeOtherNestedCondition = false;
    static void Main(string[] args)
    {    
        for (int i = 0; i < 2; i++)
        {
            if (SomeCondition)
            {
                //do some things

                if (SomeOtherNestedCondition)
                {
                    //do some further things
                }
            }
            Console.WriteLine("This text appeared from the first loop.");
        }
        for (int i = 0; i < 2; i++)
        {
            if (!SomeCondition) continue;

            //do some things

            if (!SomeOtherNestedCondition) continue;

            //do some further things
            Console.WriteLine("This text appeared from the second loop.");
        }
        Console.ReadLine(); 
    }
}

输出结果为: enter image description here

1

在内存或性能方面没有区别。底层IL只是一组跳转指令,因此它们返回到循环顶部还是“else”语句都无关紧要。

您应该选择哪个更容易阅读。我个人倾向于避免使用 'continue',但如果您有多个嵌套级别,则第二个示例可能更易于理解。


1

使用continue会使得代码的大部分与常规的过程式代码一样。 否则,如果有五个检查,您将缩进方法的“主体”10或20个字符,具体取决于缩进大小,这些是您必须滚动以查看更长行的10/20个字符。


0
然而,从我的开发背景来看,前面的例子在阅读代码时更容易理解。
嗯,我认为这是你个人的观点。例如,我尽量避免这种嵌套,因为我认为它使代码更难以阅读。
如果你喜欢“before”版本而不是“after”版本,请使用它。只需配置ReSharper,使其建议您真正想要的内容。
始终记住:ReSharper是一个相当“愚蠢”的重构工具-它无法取代开发人员。它只能通过执行一些否则愚蠢的复制和粘贴工作来帮助他。 ReSharper进行的一些重构甚至会导致ReSharper建议相反的重构情况。
因此,不要将ReSharper的建议视为最佳实践,而应将其视为您可以执行的可能性。
顺便说一句:您应该考虑性能问题-如果性能确实有显着差异,我会感到惊讶。

是的,我同意ReSharper并不总是做出正确的判断。在这种情况下,我知道转换是有效的且合乎逻辑。我的问题部分原因是我们的开发团队很小,对此有不同的看法。我很想知道C#社区普遍倾向于什么。 - KP.

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