【问题标题】:Return to first element in foreach返回foreach中的第一个元素
【发布时间】:2011-07-20 13:03:23
【问题描述】:

假设我在某个集合中使用 foreach 循环移动。

在某些时候(例如,到第 4 个元素)我想返回到此集合的第一个元素(再次遍历所有元素)

如何做到这一点?

编辑

@dlev + enumerator.Reset() 评论中的解决方案

【问题讨论】:

  • 您是否有一些您编写的代码可以帮助阐明您要做什么?
  • 你为什么要这样做?
  • +1 - foreach 循环旨在防止手动迭代。

标签: c# .net collections foreach


【解决方案1】:

虽然这个解决方案并不优雅,而且很多人会告诉你它会让你面临各种危险,但你总是可以这样做:

Restart:
foreach(var item in things)
{
    DoSomething(item);
    if(WeShouldStartOver()) goto Restart;
}

这很简单,清晰且正确。 (很多人会反射性地告诉你,goto 是邪恶的产物,而且总是错误的。那些人可能会将“不优雅”和“容易被滥用”与“道德错误”混淆。)

如果您是那些认为 goto 总是错误的人之一,您可以隐藏 goto。神秘的是,当您将“goto”拼写为“while”和“break”时,它在道德上不再是错误的!

bool loopTheLoop = true;
while(loopTheLoop)
{
    loopTheLoop = false;
    foreach(var item in things)
    {
        DoSomething(item);
        if(WeShouldStartOver())
        {
            loopTheLoop = true;
            break;
        }
    }
}

那更长,有更复杂的控制流程,并且使用数据标志只是为了控制流程,而不是在程序的业务领域中表达含义。我认为所有这些事情都和“goto”一样糟糕,但有些人真的很喜欢这种事情。

我个人倾向于将循环重构为辅助方法:

while(LoopTheLoop()) {}
...
// Returns true if we bailed early, false if we did not.
bool LoopTheLoop()
{
    foreach(var item in things)
    {
        DoSomething(item);
        if(WeShouldStartOver()) return true;
    }
    return false;
}

你能解释一下为什么你想做这件奇怪的事吗?也许有更好的方法来做你想做的事。因为坦率地说,所有这些解决方案都很糟糕。

【讨论】:

  • 如果没有来自 OP 的更多信息,我们无法知道第一个示例是否会将他返回到第一个元素。我们生活在一个多线程时代,当你重新启动时,底层集合可能会发生变化。
  • @hoodaticus:我对你的反对感到困惑,有两个方面。首先,如果 OP 有一个特殊条件——如果有一些线程问题,或者如果他使用“goto”或其他东西,恐龙会攻击他——那么由 OP 来告诉我们这个特殊条件.如果原始海报未在问题中包含相关信息,那么关于 SO 的数百万个答案中的每一个都可能是“错误的”。如果没有给出任何特殊条件,则可以合理地假设没有相关的特殊条件。
  • 第二,解决您的具体问题:您提出的多线程代码如何知道在“break”之后和第二个“foreach”开始之前对集合进行变异是安全的,但不知何故设法在“foreach”期间对其进行变异?在迭代时改变可变集合是不合法的,因此可以合理地假设它在整个操作期间的任何时候都没有被改变,包括重新启动。您是否有某种机制让另一个线程在恰好合法时间潜入并改变集合?
  • @hoodaticus:另一个线程上的代码如何知道 Dispose 已被调用?如果没有 Dispose 方法怎么办?实现 Dispose 不需要枚举器,即使这样做,也不需要向另一个线程发出信号说“我现在正在处理,继续你的待定编辑”。
  • 关于您断言“线程安全”序列在存在未完成的枚举时会锁定自己:您确定吗?这似乎是一种极其危险的模式。应该尽可能短地取出锁,但是在迭代器的创建和销毁之间可能会经过任意多的时间。应该很好地理解在获取锁和释放锁之间运行的所有代码,以确保正确排序锁,但枚举通常会产生任意外部代码。你所描述的似乎很不安全。
【解决方案2】:

改用 for 循环。或者一个while循环。

【讨论】:

  • @Saint foreach 编译成 while 循环:msdn.microsoft.com/en-us/library/aa664754%28v=vs.71%29.aspx
  • 如果是IEnumerable(枚举器)而不是collection?
  • @polishchuk:然后调用IEnumerator.Reset(假设实现了Reset)。在while 循环中很容易做到这一点。
  • @Saint “但它必须是一个 foreach 循环” - 为什么?此评论使问题看起来像拖钓。
  • 解决方案 = 评论 @dlev + enumerator.Reset()
【解决方案3】:

不要认为有任何内置但如何...

bool tryAgian = false;
do
{
   tryAgian = false;
   foreach(var item in items)
   {
      //do something
      if(needToStartAgain)
      {
         tryAgain = true;
         break;
      }
   }
} while(tryAgain)

编辑:优化

【讨论】:

    【解决方案4】:

    据我所知,你真的做不到。

    只需使用不同的循环,例如while,您可以在其中控制迭代器并决定是增加还是减少它等。

    【讨论】:

      【解决方案5】:
      for(int i = 0;i<YourCollection.Count ; i++)
      {
          if(YouNeedToStartOver)
          {
              i=-1;
              continue;
          }
      }
      

      【讨论】:

      • 可能想用那个 for 循环再试一次
      • +1 为简单起见,但如果您没有基于索引的集合访问权限,显然这不起作用。
      【解决方案6】:

      foreach 中,您只能使用continue 转到下一个元素或完全跳出循环。您需要直接使用迭代器。

      【讨论】:

        【解决方案7】:

        你可以做这样的事情

           private static void LoopMethod()
                {
                   i= 0;
                    foreach (string str in jack)
                    {
                        if (i == 4)
                            LoopMethod();
        
                        Console.WriteLine(str);
                        i++;
                    }
                }
        

        【讨论】:

        • 这不会重新启动。它只会开始另一个枚举,并在第二个完成后继续第一个。
        • @Jack7:示例:jack 包含 8 个字符串:“1”、“2”、“3”、...、“7”、“8”。假设 if 由于另一个条件只执行一次,您的代码将输出“1”、“2”、“3”、“4”、“5”、“1”、“2”、“3” 、“4”、“5”、“6”、“7”、“8”、“6”、“7”、“8”。最后三个数字是问题所在。他们不应该被退回。
        • @Daniel,结果不会是这样
        • @musefan:你是对的,结果将是“1”、“2”、“3”、“4”、“1”、“2”、“3”、“4” 、“5”、“6”、“7”、“8”、“5”、“6”、“7”、“8”
        • @Daniel,更好,但还不行。尝试...“1”、“2”、“3”、“4”、“1”、“2”、“3”、“4”、“1”、“2”、“3”、“4” ”、“1”、“2”、“3”、“4”、“1”、“2”、“3”、“4”、“1”、“2”、“3”、“4”、 “1”、“2”、“3”、“4”、“1”、“2”、“3”、“4”……一直到程序因堆栈溢出异常而崩溃
        【解决方案8】:

        如果我是你,我会考虑创建和使用集合的枚举器。

            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
        
                while (enumerator.MoveNext())
                {
                    if (SatisfyResetCondition(enumerator.Current))                          
                        enumerator.Reset();
                    Console.WriteLine(enumerator.Current);
                }
            }
        

        【讨论】:

          【解决方案9】:

          您不能对集合使用 foreach 循环执行此操作,因为当您使用 foreach 循环时,您无法访问正在使用的 Enumerator(它被编译器隐藏 - 这是 foreach 循环的全部要点!),并且集合不一定具有用于直接访问元素的索引器。

          即使可以,IEnumerator.Reset 的接口协定也指定您根本不必实现 Reset,而是可以抛出异常。这意味着对于所有集合,您不能保证 Enumerator.Reset 会让您返回到集合的开头。

          所以不,你不能做你在问题中提出的问题;这是不可能的。

          【讨论】:

          • 不是选民,但就解决问题而言,您的回答并没有太大帮助。虽然您完全正确,他不能rewind他所处的循环,但他当然可以restart它,正如其他答案所证明的那样。
          • 感谢您的解释。这是我的。没有办法用 OP 给我们的事实返回到第一个元素。我们不知道集合是否可以更改,可能在另一个线程中,因此无法以任何保证的方式再次到达第一个元素。我应该对忽略这个基本事实的每个答案投反对票。
          • 正如我在对我的回答的评论中指出的那样,假设这里没有多线程是合理的——既因为原始海报没有说明这是一个多线程场景,也因为假设多线程代码在迭代时遵守规则并且不改变集合是合理的。所以让我们把多线程排除在外。除非另有说明,否则假设两次枚举相同的集合会给出相同的结果也是合理的。但是,嘿,让我们一起来看看你关于这种情况“不可能”的说法。
          • 假设您有一个集合,当它被枚举两次时,会给出不同的结果。让我们进一步假设原始发布者希望枚举这个疯狂的集合 两次 并获得相同的结果,即使该集合没有两次返回相同的结果。难道真的不可能制作一个可以被枚举两次并且与原序列一致的可枚举对象吗?例如,如果我们可以假设该序列是有限的,那么该序列可以被枚举一次并存储在一个列表中。如果序列是无限的,那么事情会变得有点棘手。
          猜你喜欢
          • 2012-04-10
          • 1970-01-01
          • 1970-01-01
          • 2017-04-16
          • 2022-01-22
          • 2019-03-22
          • 2017-03-13
          • 2017-03-29
          相关资源
          最近更新 更多