如何在C#中不使用标志变量打破两个循环?

56

作为一个简单的例子,假设我有以下网格,并且正在查找特定单元格的值。当找到后,我不再需要处理循环。

foreach(DataGridViewRow row in grid.Rows)
{
    foreach(DataGridViewCell cell in row.Cells)
    {
        if(cell.Value == myValue)
        {
            //Do Something useful
            //break out of both foreach loops.
        }
    }
}

在C#中该如何实现这个功能?在Java中我可以使用label来命名最外层的循环,然后break该循环,但是似乎在C#中找不到相应的等价物。

怎样用最简洁的方式在C#中实现这个功能?我知道我可以设置一个布尔标志,并在外部循环中检查它以跳出该循环,但这似乎过于冗长。

谢谢。


这个例子有一些有用的反对意见,但我认为一般的问题并不总是能够被很好地重构。例如,我来到这里是因为我的小解析器方法在 while 循环内部有一个 switch(状态)。当然,如果我将所有的 switch case 转换为 if...else if,我可以使用 break,但我宁愿不这样做。将 switch 移动到函数中可以工作,但需要传递大量参数(并减少性能?)。我嗅了嗅鼻子,选择了 goto。我认为布尔标志(加上 continue 去到 while 条件?)是另一个最佳选项。 - Jon Coombs
17个回答

92

1

foreach(DataGridViewRow row in grid.Rows)
   foreach(DataGridView cell in row.Cells)
      if (cell.Value == somevalue) {
         // do stuff
         goto End;
      }
End:
   // more stuff

2

void Loop(grid) {
    foreach(row in grid.Rows)
       foreach(cell in row.Cells)
           if (something) {
               // do stuff   
               return;
           }
}

3

var cell = (from row in grid.Rows.OfType<DataGridViewRow>()
            from cell in row.Cells.OfType<DataGridViewCell>()
            where cell.Value == somevalue
            select cell
   ).FirstOrDefault();

if (cell != null) {
   // do stuff
}

78
+1 表示支持使用 goto。虽然现今人们对于 goto 操作感到害怕,但在某些情况下,它是一种合理的使用方式。 - rlbond
8
我曾经犯了一个错误,当我提到使用goto解决类似的问题时,遭到了负评。但实际上,使用break关键字跟调用goto到循环结尾几乎是一样的,不是吗? - Meta-Knight
2
@Meta-Knight:在此期间,从函数中返回多个值相当于使用goto。它只是不那么混乱,因为它施加了一些限制,你不能向上跳转或跳过一个goto标签(这两种类型很快就会退化成混乱)。 - Jimmy
2
@Jimmy,每个控制结构都等同于一个goto。同样,你用Python、Ruby、C#或Lisp编写的每个程序都可以在汇编代码或原始机器代码中完全复制。语言是构造物,它们弥合了计算硬件和人类开发者之间的差距,重要的是要认识到从函数返回值是一种设计模式,它在使用方式上与简单的goto有很大不同的影响,即使它们在底层实现上有相似之处。 - Wedge
3
我认为Jimmy所提出的主张在消极意义上是部分等效的,因为带有很多返回语句的方法流程很难阅读。我不确定是否完全同意(有时早期返回似乎比将方法的其余部分包装在另一个if块中更简洁),但总体而言,我认为避免多个返回语句是一种好习惯。其他控制结构不一定会像goto语句那样带来消极影响。 - Jon Coombs

61
尽管上面许多解决方案是正确的并回答了你的问题,但我会退后一步,问自己:“是否有另一种方式更清晰地表示程序的语义?”
我倾向于像这样编写代码:
var query = from row in grid.Rows
            from cell in row.Cells
            where cell.Value == myValue
            select cell;
if (query.Any())
{
  // do something useful;
}

为什么需要编写循环?你想知道特定的集合是否有特定的成员,所以编写一个查询来询问这个问题,然后检查答案。


但在这种情况下,我检查了所有的项目。这样做并不好,因为我只需要第一个。 - RredCat
11
@RredCat: 对不起,我不明白你的反对意见。您是在反对 (1) 你需要匹配到的第一项,而不是是否存在这样的项吗?如果是这样,请使用“FirstOrDefault”而不是“Any”。或者 (2) “Any”即使第一个项目有效也会检查每个项目?为什么您认为“Any”会这样做?它会在找到一个符合条件的元素后停止搜索。 - Eric Lippert
你说得对,我忘记了“Any()”。Linq是按请求执行的(而不是在初始化时)- 我错过了它。 - RredCat
没错!当别人已经为你编写了循环时,为什么还要自己写呢! - Suncat2000

39
最好的方法是将第二个循环拆分成一个函数,代码如下:
public void DoubleLoop()
{
    for(int i = 0; i < width; i++)
    {
        for(int j = 0; j < height; j++)
        {
            if(whatever[i][j]) break; // let's make this a "double" break
        }
    }
}
去往。
public bool CheckWhatever(int whateverIndex)
{
    for(int j = 0; j < height; j++)
    {
        if(whatever[whateverIndex][j]) return false;
    }

    return true;
}

public void DoubleLoop()
{
    for(int i = 0; i < width; i++)
    {
        if(!CheckWhatever(i)) break;
    }
}

当然,你可以使用 LINQ 或者其他方法简化这段代码(你也可以将 CheckWhatever 放在循环条件中)。这只是一个详细的原理演示。


获取示例添加的接受标志。感谢所有回复的人。 - Matthew Vines
1
这与我的答案略有不同,我的答案是您可以将两个循环合并到一个函数中,而这里有两个函数,每个函数只包含一个循环。 - JB King
我认为最简单的解决方案是Eric的LINQ方案。然而,假设我们使用的是.NET 2而没有LINQ,我会认为将OP原始示例代码整个移动到另一个方法中,并使用“return”来跳出循环要比将循环分成两个方法更清晰,因为这两个方法最终会使意图变得难以理解。 - Damian Powell
这怎么比goto好了?简直是噩梦。 - Thorham

23

我会将循环封装成一个函数,并让函数返回值作为退出循环的方式来解决问题。


1
你比我快几秒钟,但我写了一个好的例子,所以我的留下来了! - mqp
3
为什么要踩他的票?他只是在说mquander建议的话,只是没有给出代码示例。 - Matthew Vines
2
这种方法并不总是完全可行,特别是如果您在循环中对单元格进行了复杂的评估,并且这些评估取决于方法中的其他因素。 - Paul Sonier
4
我觉得让提问者自己编写代码是一项练习。 :) - JB King
@McWafflestix,这种方法要好得多。如果你的代码越来越混乱,那就说明你做错了什么。可读性强的干净代码通常会有较少的缺陷,因为它更容易测试,也更容易发现错误。 - Wedge

21
        foreach (DataGridViewRow row in grid.Rows)
        {
            foreach (DataGridViewCell cell in row.Cells)
            {
                if (cell.Value == myValue)
                {
                    goto EndOfLoop;
                    //Do Something useful
                    //break out of both foreach loops.
                }
            }

        }
        EndOfLoop: ;

那样做是可行的,但我建议使用布尔标志。

编辑: 只是想再添加一些警告; 通常认为使用goto是不好的实践,因为它们很快会导致意大利面式代码,几乎无法维护。 话虽如此,它被包含在C#语言中,并且可供使用,因此显然有人认为它具有有效的用途。 知道该功能存在并谨慎使用。


1
尽管我自己从不使用goto,但我也在想同样的问题... PeterAllenWebb的答案基本相同,却获得了2个赞同。这个答案实际上更好(提供了一个使用示例)。奇怪。 - Mark Carpenter
2
使用过多的布尔标志可能导致代码难以阅读和维护。还有更多的石头要扔吗? - Suncat2000

15

为了完整起见,还有一种错误的方法:

try
{
    foreach(DataGridViewRow row in grid.Rows)
        foreach(DataGridViewCell cell in row.Cells)
            if(cell.Value == myValue)
               throw new FoundItemException(cell);
}
catch (FoundItemException ex)
{
    //Do Something useful with ex.item
}

不是错误,只是不太理想,特别是如果你想保持良好的性能。 - Suncat2000

13

7
我本打算建议这个方法,直到我想起了 http://xkcd.com/292/。 但是没错,使用goto语句可以解决这个问题。 - Chet
2
它在MSDN中使用并不意味着这是做事情的最佳方式(就像许多其他示例一样),我仍然持有尽可能避免使用goto的观点,这个问题可以用其他方法解决(主要是将逻辑提取到单独的函数中并返回该函数)。 - zappan
1
@zappan:当然问题可以用其他方法解决。但使用“goto”是最简洁的方式。 - Suncat2000

8

最好的方式是不这样做。认真地说,如果你想在嵌套循环中找到第一个出现的东西,然后结束查找,那么你想要做的就是不检查每个元素,而这正是foreach结构明确要做的事情。我建议使用一个常规的for循环,并在循环不变式中设置终止标志。


5

以下是针对for循环的另一种解决方案:

bool nextStep = true;
for (int x = 0; x < width && nextStep; x++) {
    for (int y = 0; y < height && nextStep; y++) {
        nextStep = IsBreakConditionFalse(x, y);
    }
}

2
不错的解决方案!易读且易懂,适合那些避免使用goto语句的人。 - Suncat2000

5

你可以编写一个实现IEnumerator<T>的类,然后你的枚举代码将如下所示:

foreach (Foo foo in someClass.Items) {
    foreach (Bar bar in foo.Items) {
        foreach (Baz baz in bar.Items) {
            yield return baz;
        }
    }
}

// and later in client code

MyEnumerator e = new MyEnumerator(someClass);
foreach (Baz baz in e) {
    if (baz == myValue) {
        // do something useful
        break;
    }
 }

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