【问题标题】:Is there a "smart" way to break out of nested loops? [closed]有没有一种“聪明”的方式来打破嵌套循环? [关闭]
【发布时间】:2017-04-08 15:33:40
【问题描述】:

(现在我主要使用 C#。欢迎其他语言的想法,但如果可以的话,请将它们翻译成 C#,并且要明确。)

我一次又一次遇到的是一个嵌套循环,它在一些二维数组中搜索以找到一个元素(通常是某个对象),然后必须对其进行操作。所以当然,一旦你找到了那个对象,你应该跳出两个循环,这样你就不会不必要地继续搜索你已经找到的东西(尤其是在可以遍历指数级巨大数组的嵌套循环中)。

以下代码是目前我的首选方式:

Obj O = null;
bool KeepLooping = true;

for (int j = 0; j < height && KeepLooping; j++)
{
    for (int i = 0; i < width; i++)
    {
        if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
        {
            O = ObjArray[i, j]; // you found it, now remember it
            KeepLooping = false; // clear the flag so the outer loop will break too
            break;
        }
    }
}

感谢 Erik Funkenbusch,如果我们这样做,它会变得更加优雅:

Obj O = null;
for (int j = 0; j < height && O == null; j++) // much, much better idea to check O for null in the outer loop
{
    for (int i = 0; i < width; i++)
    {
        if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
        {
            O = ObjArray[i, j]; // you found it, now remember it
            break;
        }
    }
}

不再需要那个讨厌的额外布尔值!

尽管如此,寻找替代或更好的解决方案的工作仍在继续。多年来,我尝试了许多其他方法,但由于某种原因发现它们并不是那么好:

  1. 将j(外循环迭代器)设置为高于height 的值,这将触发它自动中断。不理想,因为有时您想记住您在其中找到它的 i 和 j 值。
  2. 在二维数组上使用foreach。不理想,因为foreach 不会让您操作集合(删除或添加到它,这通常是我搜索对象的原因)。
  3. 只需将 2 个循环放在一个除了查找并返回 O 之外什么都不做的函数中。 return 有效地打破了两个循环。很多时候这没关系,但并非总是如此。我这样做是为了非常通用的搜索,但是还有很多“组搜索”我想将遍历集中起来。在这些情况下,我会找到 2 个或更多对象(有时在同一个 2D 数组中),记住它们,然后才跳出两个循环。
  4. 使用goto? (哇,这可能是 goto 的唯一合法用法吗?它比 KeepLooping 标志更易读,尤其是当我们有 3 个或更多循环时。)不理想,因为同事会尖叫血腥谋杀。而在 C# 中,goto 之后会有正确的垃圾清理吗?
  5. 抛出自定义异常? idk,我从未尝试过,但它的可读性不如我目前首选的方式。
  6. 找到正确的对象后,在内部循环中执行所有对象操作,然后return; 这很快就会变得一团糟。有时对象操作涉及到自己的循环。

还有一个非常聪明的第七条路,感谢User_PWY:

int size = width*height; // save this so you dont have to keep remultiplying it every iteration
for (int i = 0; i < size; i++)
{
   int x = i % width; // ingenious method here
   int y = i / width; // ingenious method here

   O = ObjArray[x, y];

   if (O != null)
       break; // woohoo!
}

这有效地将二维数组压缩为一个 for 循环以进行迭代。然而,一些批评指出,与 i++ 或 j++ 相比,mod 和除法运算符相当慢,因此它可能会更慢(请记住,我们正在处理未知大小的 2D 数组)。就像我评论的那样,应该有一种方法可以在一个操作中获得除法和余数,因为我很确定 x86 汇编代码具有 DIV 操作码,它们将商和余数存储在单独的寄存器中,所有这些都在一个 DIV 指令中。但是如何在 C# 中执行/使用它,idk。

如果 C# 允许你命名循环就好了,比如L1 和L2,然后做类似L1.break() 的事情;无论您在哪个循环中。唉......它不能用这种语言完成。 (会不会有一些使用宏的秘密方法?)C# 6.0 会实现这个功能吗?

编辑:在我看来,我根据它们的优雅和速度来判断解决方案。请记住,我们正在处理嵌套循环,它可能会成倍增长。额外的操作或比较可能会有所作为。

好的,好吧,告诉我你喜欢的方式,特别是如果它没有在这里列出。

【问题讨论】:

  • goto 使用 C# 是有原因的,所以如果您认为它是 goto 的合法用例并且有性能数据来证明这一点,您的同事不应该抱怨。
  • goto 不会改变垃圾收集器的工作方式。
  • @zzzzBov 好的,但很难告诉同事他们应该抱怨什么,不应该抱怨什么,除非你是老板(我不是):/
  • 当你有证据时,很容易告诉你的同事他们不能抱怨。那么你所需要的只是证明他们有更好的方法的证据。如果他们这样做,请改用它。

标签: c# loops


【解决方案1】:

goto 是一个完美的解决方案,微软甚至 recommends it:

goto 语句直接将程序控制转移到一个有标签的 声明。

goto 的一个常见用途是将控制权转移到特定的 switch-case 标签或 switch 语句中的默认标签。

goto 语句对于跳出深度嵌套的循环也很有用。

至于您关于对象销毁的问题,一旦您超出范围对象的范围,垃圾收集器应该将其标记为销毁,但我不能确定如果您使用行为是否相同,例如,一个 using 指令围绕您的 for 循环和 goto 退出它,而不是正常退出范围。

【讨论】:

    【解决方案2】:
    for (int i = 0; i < width*height; i++)
    {
       int x=i%width
          ,y=i/width;
       //dostuff
    }
    

    我喜欢这种访问二维数组的方式。

    评论 1)
    有很多 cmets 担心 mod(%) 运算符的成本可能很高,但这是我们正在谈论的整数运算,我认为其他解决方案应该没有区别。

    评论 2)
    关于宏。我找不到代码,但设法生成了一个。

    #define FOR2DARRAY(WIDTH,HEIGHT) 
        \for (int i = 0, x = 0,y = 0; i < (WIDTH)*(HEIGHT); i++, x=i%(WIDTH),y=i/(HEIGHT))
    

    【讨论】:

    • 不错。它似乎有点熟悉,我可能曾经使用过一两次来将数组用于其他目的。现在我真的很想翻阅我的档案……但我确定我从未见过或听说过它会破坏嵌套循环,所以我标记了它。这适用于任何宽度 x 高度吗?还是必须是正方形?
    • 这个解决方案的一个问题是,现在您在每次迭代中执行两个相对昂贵的操作(除法和余数)而不是一个便宜的操作(加法)。这对性能敏感的循环来说可能很糟糕。
    • iirc,mod 运算符一点也不贵。师当然是。至于循环头中的乘法,我们可以做类似int size = width*height这样的事情,所以我们只做一次乘法,然后记住它,并在循环头中使用size而不是更字面的witdth*height。
    • @DrZ214 我的理解是余数与除法的费用相似。但是on x86 余数是除法的副产品,因此如果 JIT 将其优化为一个操作,我不会感到惊讶(尽管其中一个 cmets 表明它不在 .NET 中)。是的,我故意忽略了乘法,因为它很容易被删除。
    • @DrZ214 你会认为会有,但我不认为有。有一个吸引人的名字Math.DivRem,但是人们有opened issues,因为它有效地执行了两次基础除法。 source 与此代码非常相似,因此 JIT 似乎没有优化这种情况。希望它会尽快得到解决。
    【解决方案3】:

    重构以避免深度嵌套,使用 LINQ 可能是更好的解决方案。

    Goto 在这种情况下是可能的(这基本上只是使用它的唯一情况)如果你不能想出更好的方法。使用可读名称设置标志可能会减少问题/尖叫。

    不要

    • 对流控制使用例外
    • 更改循环变量。在这种情况下,弄清楚发生了什么是非常困难的。我敢打赌,大多数人会接受goto 作为更好的方法。

    【讨论】:

    • 在 LINQ 中打破嵌套非常棘手,没有例外。你能举个例子吗?
    • @Gabe 一个选项 - SelectMany 用于展平嵌套循环,TakeWhile/SkipWhile/First/Where 用于过滤/缩短循环
    【解决方案4】:

    差别不大,但我更喜欢以这种方式使用布尔标志:

    Obj O = null;
    bool found = false;
    
    for (int j = 0; j < height; j++)
    {
        for (int i = 0; i < width; i++)
        {
            if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value)
            {
                O = ObjArray[i, j]; // you found it, now remember it
                found = true; // clear the flag so the outer loop will break too
                break;
            }
        }
        if (found) break;
    }
    
    if (!found)
    {
        // The loop finished with no values found
    }
    else
    {
        // Do stuff here with the value found
    }
    

    【讨论】:

      【解决方案5】:

      试试这个:

      Obj O = ObjArray.Cast<Obj>().FirstOrDefault(obj => obj != null && obj.property == search_value);
      

      这会将Obj 的二维数组转换为IEnumerable&lt;Obj&gt;,并为您提供第一个符合您条件的数组。如果它们都不匹配,则将Obj O 设置为null。

      在这里查看:https://dotnetfiddle.net/dQTJbU

      【讨论】:

        【解决方案6】:

        也许我在您的选项列表中错过了它,但是您为什么不能将搜索重构为返回对象的方法?

        private object FindObject(....)
        {
            for (int j = 0; j < height && KeepLooping; j++)
            {
                for (int i = 0; i < width; i++)
                {
                    if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value)
                    {
                        return ObjArray[i, j]; // you found it, now remember it
                    }
                }
            }
            return null;
        } 
        

        【讨论】:

        • 是的,这就是我列出的第 3 个解决方案。我大约有一半时间使用它,但它并不总是足够的。原因请参见列出的第 3 项。
        • @DrZ214 - 好的,但我不明白为什么,如果你想要更多对象,只需让函数返回一个列表。
        【解决方案7】:

        有很多方法可以做到这一点。在您的示例中,我会这样做:

        Obj O = null;
        
        for (int j = 0; j < height && O == null; j++)
        {
            for (int i = 0; i < width; i++)
            {
                if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
                {
                    O = ObjArray[i, j]; // you found it, now remember it
                    break;
                }
            }
        }
        

        在这种情况下不需要 KeepLo​​oping 变量,因此简单的 break 语句可以非常优雅地工作。

        实际上你甚至可以进一步简化:

        Obj O = null;
        
        for (int j = 0; j < height && O == null; j++)
        {
            for (int i = 0; i < width && O == null; i++)
            {
                if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
                    O = ObjArray[i, j]; // you found it, now remember it
            }
        }
        

        现在,甚至不需要休息。

        当然,这可能不适用于所有情况,但通常您可以将结果类型用作状态值。

        仅供参考,如果您正确执行,在 foreach 中修改集合的问题并不是真正的问题。例如:

        // Assumes ObjArray is [,] and not [][]
        foreach (int item in ObjArray.ToList()) // makes a copy to iterate over
        {
            if (item != null && item.property == search_value) {
                ObjArray[0,0] = item; // I can now modify this for no reason
                break;
            }
        }
        

        【讨论】:

        • 嘿,感谢使用&amp;&amp; O == null 而不是KeepLooping 的更短的方法!没有额外的布尔值就更优雅了。但我不会像你的第二个例子那样做。 break; 通过立即中断为我们节省了一些额外的指令,而不必返回进行比较。此外,将&amp;&amp; O == null 放在内部循环中也意味着我们在每次迭代时都进行比较,实际上,它只需要在外部循环中。
        • @DrZ214 - 你没有明确表示你关心一些额外的 cpu 周期。尽管如此,只要是内在类型,比较操作就很便宜。
        • 好的,我回去说清楚了。我喜欢您的第一个解决方案并将其添加到我的 OP 中。
        猜你喜欢
        • 1970-01-01
        • 2020-08-28
        • 1970-01-01
        • 2015-04-07
        • 2015-12-19
        • 2012-02-15
        • 2021-03-02
        • 2012-01-19
        相关资源
        最近更新 更多