2016-05-31 107 views
2

根据MS documentation,枚举器应该抛出InvalidOperationEx,如果底层的枚举源被修改。这工作时,我直接从IEnumerable获得枚举数。但是,如果我从“查询数据结构”获取枚举数,然后修改源并调用MoveNext(),则不会引发任何操作(请参阅代码)。LINQ中枚举器的怪异行为

考虑下面的代码:

public static void Main(string[] args) 
    { 
     var src = new List<int>() { 1, 2, 3, 4 };   
     var q = src.Where(i => i % 2 == 1); 
     IEnumerable<int> nl = src; 
     var enmLinq = q.GetEnumerator(); 
     var enmNonLinq = nl.GetEnumerator(); 

     src.Add(5); //both enumerators should be invalid, as underlying data source changed  

     try 
     { 
      //throws as expected 
      enmNonLinq.MoveNext(); 
     } 
     catch (InvalidOperationException) 
     { 
      Console.WriteLine("non LINQ enumerator threw..."); 
     } 

     try 
     { 
      //DOES NOT throw as expected 
      enmLinq.MoveNext(); 
     } 
     catch (InvalidOperationException) 
     { 
      Console.WriteLine("enumerator from LINQ threw..."); 
     } 

     //It seems that if we want enmLinq to throw exception as expected: 
     //we must at least once call MoveNext on it (before modification) 
     enmLinq.MoveNext(); 
     src.Add(6); 
     enmLinq.MoveNext(); // now it throws as it should 
    } 

看来你必须先调用的MoveNext()方法来使它注意到底层源的变化。

为什么我认为这正在发生的事情: 我认为这是因为“查询结构”是给你懒得枚举中,这反而对的GetEnumerator(初始化)MoveNext的第一次调用时被初始化()

通过初始化,我的意思是连接所有的枚举器(从WhereEnumerable,SelectEnumerable等LINQ方法返回的结构),直到真正的底层数据结构。

问题: 我说得对吗?或者我错过了什么? 你认为这是奇怪/错误的行为?

回答

4

你是对的。

LINQ查询不会叫GetEnumerator底层List<T>上,直到您对Where返回IEnumerable<T>调用MoveNext

可以在reference sourceMoveNext被实现像这样见:

public override bool MoveNext() 
{ 
    switch (state) 
    { 
     case 1: 
      enumerator = source.GetEnumerator(); 
      state = 2; 
      goto case 2; 
     case 2: 
      while (enumerator.MoveNext()) 
      { 
       TSource item = enumerator.Current; 
       if (predicate(item)) 
       { 
        current = item; 
        return true; 
       } 
      } 
      Dispose(); 
      break; 
    } 
    return false; 
} 

在“初始”状态(状态1),它将首先请sourceGetEnumerator移动至状态2。

之前
+0

谢谢你,你认为它是MS实施中的错误/不一致吗? – videokojot

+0

@videokojot我会说这是由设计。 LINQ通常尽可能延迟,直到这一点,它不需要源枚举器(例如,您可能永远不会枚举查询)。 –

+0

好的,谢谢。公认。尽管如此,我觉得有点奇怪,但生病了。 – videokojot

-1

你可以自己测试一下。

public static void Main(string[] args) 
    { 
     var src = new List<int>() { 1, 2, 3, 4 }; 
     var q = src.Where(i => 
     { 
      Output(); 
      return i % 2 == 1; 
     } 
     ); 

     IEnumerable<int> nl = src; 
     var enmLinq = q.GetEnumerator(); 
     var enmNonLinq = nl.GetEnumerator(); 

     src.Add(5); //both enumerators should be invalid, as underlying data source changed  

     try 
     { 
      //throws as expected 
      enmNonLinq.MoveNext(); 
     } 
     catch (InvalidOperationException) 
     { 
      Console.WriteLine("non LINQ enumerator threw..."); 
     } 

     try 
     { 
      //DOES NOT throw as expected 
      // Output() is called now. 
      enmLinq.MoveNext(); 
     } 
     catch (InvalidOperationException) 
     { 
      Console.WriteLine("enumerator from LINQ threw..."); 
     } 

     //It seems that if we want enmLinq to throw exception as expected: 
     //we must at least once call MoveNext on it (before modification) 
     enmLinq.MoveNext(); 
     src.Add(6); 
     enmLinq.MoveNext(); // now it throws as it should 
    } 

    public static void Output() 
    { 
     Console.WriteLine("Test"); 
    } 

当你运行程序,你会看到“测试”是不是输出到控制台,直到你打电话给你的第一MoveNext的,之后源最初修改发生后。

+0

多数民众赞成清楚(谓词在哪里只调用MoveNext时调用),但我没有看到任何连接到我的问题。 – videokojot

1

enmLinq直到第一个MoveNext调用才被实现。因此,在致电MoveNext之前对src所做的任何修改都不会影响enmLinq的有效性。一旦您拨打MoveNextenmLinq - 枚举器已实现,因此src上的任何更改都将导致后续MoveNext调用异常。

2

该文件仅指出execution is deferred until the object is enumerated either by calling its GetEnumerator method directly or by using foreach in Visual C# or For Each in Visual Basic

由于它缺乏更详细地,通过执行LINQ查询可以调用自身的源GetEnumerator无论是在第一次调用他们自己的GetEnumerator或尽可能晚地,如在第一次调用MoveNext

我不会承担任何特定的行为。

实际中,实际执行(见参考源中的Enumerable.WhereEnumerableIterator<TSource>defers execution to the first call to MoveNext