【问题标题】:Does foreach evaluate the array at every iteration?foreach 是否在每次迭代时评估数组?
【发布时间】:2013-11-20 22:22:49
【问题描述】:

我想创建一个跳过第一项的foreach。我在其他地方看到过,最简单的方法是使用myCollection.Skip(1),但我有一个问题:

.Skip() 上的 MSDN 文档描述它“绕过序列中指定数量的元素,然后返回剩余的元素”。这是否意味着调用

foreach(object i in myCollection.Skip(1))
{ ... }

程序是否必须在每次foreach 迭代时执行.Skip(1)?还是foreach(有点像switch)不需要对数组进行多次评估?

创建一个虚拟的var _dummy = myCollection.Skip(1) 并对其进行迭代会更有效吗?

【问题讨论】:

  • Skip 返回一个 Enumerable。这是用于生成枚举器的内容。 foreach 使用枚举器从枚举器中的每个可枚举对象中获取类型对象。
  • 我在这里可能有点离谱,但似乎一个简单的解决方案(而不是使用Skip)是使用标准的for 循环并让索引从1 开始?
  • @MirroredFate 是的,那行得通。然而,foreach 的部分意义在于它提高了可读性,这正是我所希望的。

标签: c# arrays foreach


【解决方案1】:

您应该了解foreach 的工作方式。这个 foreach 循环:

foreach(T t in GetSomeEnumerable())
    DoSomethingWithT(t);

等价于这段代码:

var e = GetSomeEnumerable().GetEnumerator();
try{
    while(e.MoveNext()){
        T t = (T)e.Current; // unless e is the generic IEnumerator<T>,
                            // in which case, there is no cast
        DoSomethingWithT(t);
    }
}finally{
    if(e is IDisposable)
        e.Dispose();
}

【讨论】:

  • 我写的答案差不多。 FWIW,5.0 规范在第 247 页对此进行了说明。这个答案比 IL IMO 更容易理解。
  • 同意.. 接受 IL 的答案使它看起来“准确”我猜.. 但恕我直言,它脱离了上下文,了解 foreach 的工作原理更重要.. 比查看 IL跳过通话。
【解决方案2】:

Skip 的整个表达式只会被调用一次。 Skip 使用延迟执行,以便在存在不使用延迟执行的操作时执行。在那一刻,在背景上构建了一个表达式树,如果没有任何变化,则将对 IEnumerable 实例的引用返回给使用它的调用者。

【讨论】:

    【解决方案3】:

    把它拉出来,它可能会变得更清晰。

    var myCollection = new List<object>();
    var skipped = myCollection.Skip(1);
    
    foreach (var i in skipped) {
        Console.WriteLine(i.ToString());
    }
    

    因此,skipped 只是 foreach 现在枚举的 IEnumerable

    IL 在这种情况下是这样的:

    IL_0000:  newobj      System.Collections.Generic.List<System.Object>..ctor
    IL_0005:  stloc.0     // myCollection
    IL_0006:  ldloc.0     // myCollection
    IL_0007:  ldc.i4.1    
    IL_0008:  call        System.Linq.Enumerable.Skip
    IL_000D:  stloc.1     // skipped
    IL_000E:  ldloc.1     // skipped
    IL_000F:  callvirt    System.Collections.Generic.IEnumerable<System.Object>.GetEnumerator
    IL_0014:  stloc.3     // CS$5$0000
    IL_0015:  br.s        IL_0029
    IL_0017:  ldloc.3     // CS$5$0000
    IL_0018:  callvirt    System.Collections.Generic.IEnumerator<System.Object>.get_Current
    IL_001D:  stloc.2     // i
    IL_001E:  ldloc.2     // i
    IL_001F:  callvirt    System.Object.ToString
    IL_0024:  call        System.Console.WriteLine
    IL_0029:  ldloc.3     // CS$5$0000
    IL_002A:  callvirt    System.Collections.IEnumerator.MoveNext
    IL_002F:  brtrue.s    IL_0017
    IL_0031:  leave.s     IL_003D
    IL_0033:  ldloc.3     // CS$5$0000
    IL_0034:  brfalse.s   IL_003C
    IL_0036:  ldloc.3     // CS$5$0000
    IL_0037:  callvirt    System.IDisposable.Dispose
    IL_003C:  endfinally  
    

    您的代码的 IL 看起来很相似:

    var myCollection = new List<object>();
    
    foreach (var i in myCollection.Skip(1)) {
        Console.WriteLine(i.ToString());
    }
    
    IL_0000:  newobj      System.Collections.Generic.List<System.Object>..ctor
    IL_0005:  stloc.0     // myCollection
    IL_0006:  ldloc.0     // myCollection
    IL_0007:  ldc.i4.1    
    IL_0008:  call        System.Linq.Enumerable.Skip <-- 1 Call to .Skip() outside the loop.
    IL_000D:  callvirt    System.Collections.Generic.IEnumerable<System.Object>.GetEnumerator
    IL_0012:  stloc.2     // CS$5$0000
    IL_0013:  br.s        IL_0027
    IL_0015:  ldloc.2     // CS$5$0000
    IL_0016:  callvirt    System.Collections.Generic.IEnumerator<System.Object>.get_Current
    IL_001B:  stloc.1     // i
    IL_001C:  ldloc.1     // i
    IL_001D:  callvirt    System.Object.ToString
    IL_0022:  call        System.Console.WriteLine
    IL_0027:  ldloc.2     // CS$5$0000
    IL_0028:  callvirt    System.Collections.IEnumerator.MoveNext
    IL_002D:  brtrue.s    IL_0015
    IL_002F:  leave.s     IL_003B
    IL_0031:  ldloc.2     // CS$5$0000
    IL_0032:  brfalse.s   IL_003A
    IL_0034:  ldloc.2     // CS$5$0000
    IL_0035:  callvirt    System.IDisposable.Dispose
    IL_003A:  endfinally  
    

    它仍然只有一个 .Skip() 调用。

    【讨论】:

    • 我不确定这是否能回答问题,因为它没有解决如果在 foreach 声明中调用 Skip() 会发生什么。或者确实如此,而且我很密​​集?
    • 此代码与原始问题不匹配。您需要在 foreach 中使用 skipped 而不是 myCollection
    • @Mr.Mindor:阅读第二个例子。
    • 不确定你的意思。在第一个例子中,“把它拉出来,它可能会变得更清晰。”通常,当我在解释中看到“拉出来”时,期望代码是等效的。您的第一个示例并非如此,它遍历整个集合。
    • @Mr.Mindor:是的。我有一个错字。谢谢。
    【解决方案4】:

    我只是用这个模拟了你的代码

    foreach(var v in Enumerable.Range(1,10).Skip(1))
        v.Dump();
    

    这是生成的 IL。

    IL_0001:  nop         
    IL_0002:  ldc.i4.1    
    IL_0003:  ldc.i4.s    0A 
    IL_0005:  call        System.Linq.Enumerable.Range
    IL_000A:  ldc.i4.1    
    IL_000B:  call        System.Linq.Enumerable.Skip//Call to Skip
    IL_0010:  callvirt    System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator
    IL_0015:  stloc.1     // CS$5$0000
    IL_0016:  br.s        IL_0026
    IL_0018:  ldloc.1     // CS$5$0000
    IL_0019:  callvirt    System.Collections.Generic.IEnumerator<System.Int32>.get_Current
    IL_001E:  stloc.0     // v
    IL_001F:  ldloc.0     // v
    IL_0020:  call        LINQPad.Extensions.Dump
    IL_0025:  pop         
    IL_0026:  ldloc.1     // CS$5$0000
    IL_0027:  callvirt    System.Collections.IEnumerator.MoveNext
    IL_002C:  stloc.2     // CS$4$0001
    IL_002D:  ldloc.2     // CS$4$0001
    IL_002E:  brtrue.s    IL_0018
    IL_0030:  leave.s     IL_0042
    IL_0032:  ldloc.1     // CS$5$0000
    IL_0033:  ldnull      
    IL_0034:  ceq         
    IL_0036:  stloc.2     // CS$4$0001
    IL_0037:  ldloc.2     // CS$4$0001
    IL_0038:  brtrue.s    IL_0041
    IL_003A:  ldloc.1     // CS$5$0000
    IL_003B:  callvirt    System.IDisposable.Dispose
    IL_0040:  nop         
    IL_0041:  endfinally  
    

    如您所见,Skip 只被调用一次。

    等效的 c# 代码如下所示

    IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();//Get the enumerator
    try
    {
      int m;//This variable is here prior to c#5.0
      while(e.MoveNext())
      {//int m; is declared here starting from c#5.0
        m = (int)(int)e.Current;
        //Your code here
      }
    }
    finally
    {
      if (e != null) ((IDisposable)e).Dispose();
    }
    

    考虑下面的代码,如果 foreach 在每次迭代中调用VeryLongRunningMethodThatReturnsEnumerable,那将是一场噩梦。语言设计上的巨大缺陷。幸运的是它没有这样做。

    foreach(var obj in VeryLongRunningMethodThatReturnsEnumerable())
    {
       //Do something with that obj
    }
    

    【讨论】:

    • 谢谢,这样就搞定了。只是在进行逐步调试时,我可以看到myCollection.Skip(1) 被“突出显示”以进行评估。我只是不确定这是为了评估myCollection.Skip(1).MoveNext() 还是仅仅为了评估_dummy.MoveNext(),其中_dummy 是在调用foreach 循环之前秘密创建的匿名集合。感谢您清除它。看到你的编辑,这正是我害怕的。
    • @ps06756 这是在LinqPad 的帮助下,有许多可用的反编译器,Reflector、IL-Spy、DotPeek、JustDecompile 等
    【解决方案5】:

    你的迭代是命令的结果:

    myCollection.Skip(1)
    

    这有效地返回了myCollection 类型的IEnumerable,它省略了第一个元素。因此,您的 foreach 反对缺少第一个元素的新 IEnumerableforeach 强制通过枚举对产生的Skip(int) 方法进行实际评估(它的执行被推迟到枚举,就像其他 LINQ 方法如Where 等一样)它与以下内容相同:

    var mySkippedCollection = myCollection.Skip(1);
    foreach (object i in mySkippedCollection)
    ...
    

    这是Skip(int) 实际最终执行的代码:

    private static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
    {
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (count > 0 && enumerator.MoveNext())
            {
                count--;
            }
            if (count <= 0)
            {
                while (enumerator.MoveNext())
                {
                    yield return enumerator.Current; // <-- here's your lazy eval
                }
            }
        }
        yield break;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-27
      • 1970-01-01
      相关资源
      最近更新 更多