LINQ中枚举器中的奇怪行为

时间:2016-05-31 14:33:22

标签: c# .net linq

根据MS documentation,如果修改了基础枚举源,则枚举器应抛出InvalidOperationEx。当我直接从IEnumerable获取枚举器时,这可以工作。

问题:但是如果我从"查询数据结构中获取枚举器" ,然后修改源,然后调用MoveNext(),不抛出任何东西(参见代码)。

请考虑以下代码:

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()方法,使其注意到底层源的更改。

为什么我认为这种情况正在发生: 我认为这是因为&#34;查询结构&#34;给你太懒惰的枚举器,而不是在 GetEnumerator()上初始化,而是在第一次调用 MoveNext()时初始化。

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

问题: 我是对的还是我错过了什么? 你认为它是奇怪/错误的行为吗?

4 个答案:

答案 0 :(得分:4)

你是对的。

在您GetEnumerator返回的List<T>上致电MoveNext之前,LINQ查询不会在基础IEnumerable<T>上调用Where

您可以在reference source中看到MoveNext的实施方式如下:

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

在&#39;初始&#39;状态(状态1),它将首先在GetEnumerator上调用source,然后再转到状态2.

答案 1 :(得分: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,例如第一次调用{ {1}}。

我不会假设任何特定行为。

实际上,实际实施(请参阅参考来源中的Enumerable.WhereEnumerableIterator<TSource>defers execution to the first call to MoveNext

答案 2 :(得分:1)

在第一次enmLinq致电之前,

MoveNext尚未实现。因此,在调用src之前对MoveNext所做的任何修改都不会影响enmLinq的有效性。在MoveNext上致电enmLinq后 - 枚举器已实现,因此src上的任何更改都会导致后续MoveNext来电异常。

答案 3 :(得分:-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");
    }

当你运行程序时,你会看到“Test”没有输出到控制台,直到你调用你的第一个MoveNext,这是在最初修改源之后发生的。