44

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

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

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

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

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

4

5 回答 5

50

我只是用这个嘲笑了你的代码

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();
}

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

foreach(var obj in VeryLongRunningMethodThatReturnsEnumerable())
{
   //Do something with that obj
}
于 2013-11-08T19:33:17.977 回答
33

您应该了解其工作方式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();
}
于 2013-11-08T19:46:01.790 回答
7

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

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

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

所以skipped只是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() 调用。

于 2013-11-08T19:29:56.963 回答
4

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

于 2013-11-08T19:39:59.183 回答
2

你的迭代是命令的结果:

myCollection.Skip(1)

这有效地返回了省略第一个元素IEnumerable的类型。myCollection所以你的 foreach 然后反对IEnumerable缺少第一个元素的 new 。通过枚举foreach强制对产生的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;
}
于 2013-11-08T19:35:41.600 回答