foreach是否在每次迭代时评估数组?

时间:2013-11-08 19:27:07

标签: c# arrays foreach

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

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

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

每次.Skip(1)迭代时,程序都必须执行foreach吗?或者foreach(有点像switch)不需要对数组进行多次评估吗?

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

5 个答案:

答案 0 :(得分:41)

我只是用这个

嘲笑你的代码
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
}

答案 1 :(得分:26)

您应该了解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();
}

答案 2 :(得分:6)

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

var myCollection = new List<object>();
var skipped = myCollection.Skip(1);

foreach (var i in skipped) {
    Console.WriteLine(i.ToString());
}

所以跳过的只是IEnumerable foreach现在列举的内容。

这就是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()调用。

答案 3 :(得分:4)

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

答案 4 :(得分:2)

你的迭代是命令的结果:

myCollection.Skip(1)

这实际上返回了省略第一个元素的IEnumerable类型的myCollection。所以你的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;
}