【问题标题】:Weird behaviour in enumerators in LINQLINQ 中枚举器的奇怪行为
【发布时间】:2016-05-31 14:33:22
【问题描述】:

根据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()方法让它注意到底层源的变化。

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

初始化是指将所有枚举器(从 WhereEnumerable、SelectEnumerable 等 LINQ 方法返回的结构)连接到真正的底层数据结构。

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

【问题讨论】:

    标签: c# .net linq


    【解决方案1】:

    你是对的。

    LINQ 查询不会在底层List&lt;T&gt; 上调用GetEnumerator,直到您在Where 返回的IEnumerable&lt;T&gt; 上调用MoveNext

    您可以在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;
    }
    

    在“初始”状态(状态 1),它会首先在 source 上调用 GetEnumerator,然后再移动到状态 2。

    【讨论】:

    • 谢谢,您认为这是 MS 实现中的错误/不一致吗?
    • @videokojot 我会说这是设计使然。 LINQ 通常尽可能地懒惰,直到此时它才需要源枚举器(例如,您可能永远不会枚举查询)。
    • 好的,谢谢。公认。在我看来仍然有点奇怪,但我不能忍受。
    【解决方案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 或尽可能晚(例如第一次调用MoveNext)时在其源上调用GetEnumerator

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

    在实践中,实际实现(参见参考源中的Enumerable.WhereEnumerableIterator&lt;TSource&gt;defers execution to the first call to MoveNext

    【讨论】:

      【解决方案3】:

      enmLinq 在第一次调用 MoveNext 之前不会实现。因此,在调用MoveNext 之前对src 所做的任何修改都不会影响enmLinq 的有效性。一旦你在enmLinq 上调用MoveNext - 枚举器就会被实现,因此src 上的任何更改都会导致后续MoveNext 调用出现异常。

      【讨论】:

        【解决方案4】:

        你可以自己测试一下。

            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 之后才会输出到控制台,这发生在最初修改源之后。

        【讨论】:

        • 很清楚(只有在调用 MoveNext 时才会调用 where 中的谓词),但我看不到与我的问题有任何联系。
        猜你喜欢
        • 1970-01-01
        • 2012-11-15
        • 2017-07-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多