Does foreach evaluate the array at every iteration?

前端 未结 5 913
失恋的感觉
失恋的感觉 2020-11-27 20:34

I want to create a foreach which skips the first item. I\'ve seen elsewhere that the easiest way to do this is to use myCollection.Skip(1), but I h

相关标签:
5条回答
  • 2020-11-27 21:09

    I just mocked your code with this

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

    And here is the IL generated.

    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  
    

    As you can see Skip is called only once.

    Equivalent c# code would look something like this

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

    Consider the below code, If foreach calls VeryLongRunningMethodThatReturnsEnumerable at each iteration then that would be nightmare. Huge flaw in the design of the language. Fortunately it doesn't do that.

    foreach(var obj in VeryLongRunningMethodThatReturnsEnumerable())
    {
       //Do something with that obj
    }
    
    0 讨论(0)
  • 2020-11-27 21:10

    Pull it out and it probably becomes clearer.

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

    So skipped is just an IEnumerable that foreach is enumerates now.

    Here's what the IL looks like in that case:

    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  
    

    The IL for your code looks similar:

    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  
    

    It still has just the one .Skip() call.

    0 讨论(0)
  • 2020-11-27 21:17

    You should understand the way foreach works. This foreach loop:

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

    is equivalent to this code:

    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();
    }
    
    0 讨论(0)
  • 2020-11-27 21:17

    The whole expression with Skip will get called only once. Skip uses a deferred execution so that it is executed once there are actions that does not use deferred execution. In that moment an expression tree is builded on the background and reference to an instance of IEnumerable is yielded back to the caller who uses it if nothing changes.

    0 讨论(0)
  • 2020-11-27 21:32

    What's your iterating on is the result of the command:

    myCollection.Skip(1)
    

    This effectively returns an IEnumerable of the type of myCollection which has omitted the first element. So your foreach then is against the new IEnumerable which lacks the first element. The foreach forces the actual evaluation of the yielded Skip(int) method via enumeration (its execution is deferred until enumeration, just like other LINQ methods such as Where, etc.) It would be the same as:

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

    Here's the code that Skip(int) actually ends up performing:

    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;
    }
    
    0 讨论(0)
提交回复
热议问题