【问题标题】:Enumerator for SortedDictionary.ValueCollection behaviour differs from other EnumeratorsSortedDictionary.ValueCollection 行为的枚举器与其他枚举器不同
【发布时间】:2020-05-17 05:16:20
【问题描述】:

由于SortedDictionary.ValueCollectionEnumerator 的行为与其他枚举器的行为不同,我们最近遇到了一个错误。我设法将问题缩小到以下(无意义的)示例:

public void Example()
{
    var sorted = new SortedDictionary<string, int>
    {
        {"1", 1 },
        {"3", 3 },
        {"0", 0 },
        {"2", 2 },
        {"4", 4 }
    };

    var fromValues = sorted.Values.GetEnumerator();
    fromValues.MoveNext();

    var fromLinq = sorted.Select(x => x.Value).GetEnumerator();
    fromLinq.MoveNext();

    var fromDictionary = new Dictionary<string, int>(sorted).Values.GetEnumerator();
    fromDictionary.MoveNext();

    for (var i = 0; i < 3; i++)
    {
        Console.WriteLine($"Printing for {i}");
        Print("  From Values:     ", fromValues, i);
        Console.WriteLine("  ------------");
        Print("  From Linq:       ", fromLinq, i);
        Console.WriteLine("  ------------");
        Print("  From Dictionary: ", fromDictionary, i);
        Console.WriteLine();
    }
}

private void Print(string prefix, IEnumerator<int> enumerator, int value)
{
    do
    {
        Console.WriteLine(prefix + "Value in loop:  " + enumerator.Current);
        if (enumerator.Current == value)
        {
            Console.WriteLine(prefix + "Selected Value: " + enumerator.Current);
            break;
        }
    } while (enumerator.MoveNext());
}

这将产生以下输出:

Printing for 0
  From Values:     Value in loop:  0
  From Values:     Selected Value: 0
  ------------
  From Linq:       Value in loop:  0
  From Linq:       Selected Value: 0
  ------------
  From Dictionary: Value in loop:  0
  From Dictionary: Selected Value: 0

Printing for 1
  From Values:     Value in loop:  0
  From Values:     Value in loop:  1
  From Values:     Selected Value: 1
  ------------
  From Linq:       Value in loop:  0
  From Linq:       Value in loop:  1
  From Linq:       Selected Value: 1
  ------------
  From Dictionary: Value in loop:  0
  From Dictionary: Value in loop:  1
  From Dictionary: Selected Value: 1

Printing for 2
  From Values:     Value in loop:  0
  From Values:     Value in loop:  2
  From Values:     Selected Value: 2
  ------------
  From Linq:       Value in loop:  1
  From Linq:       Value in loop:  2
  From Linq:       Selected Value: 2
  ------------
  From Dictionary: Value in loop:  0
  From Dictionary: Value in loop:  1
  From Dictionary: Value in loop:  2
  From Dictionary: Selected Value: 2

如您所见,三个Iterators 的行为不同:

  • SortedDictionary:Current 在传递给函数时始终为 0,但 MoveNext 在循环中推送到正确的值。
  • Linq:行为与我预期的 Iterator 一致。
  • 字典:每次将 Enumerator 传递给函数时都会重置。

我怀疑SortedDictionary.ValueCollection.Enumerator 是一个结构,而 linq 产生的一个是引用类型这一事实与它有关。但这并不能解释为什么它不像 Dictionary 中的Enumerator

【问题讨论】:

  • 这确实很奇怪——.Net Core 也会发生这种情况
  • @MatthewWatson 简化复制的行为很清楚。 Enumerator 是一个struct 并且在每次调用PrintNextValue 方法时,您都会传递它的副本,其中Current 设置为0,即集合中的第一个元素。但它没有回答 OP 问题
  • 与此同时,我找到了前往Jon SkeetEric Lippert 的方法。但这并不能解释 SortedDictionary...

标签: c# .net iterator


【解决方案1】:

我相信这个问题的答案是SortedDictionary 值枚举器是一个结构(System.Collections.Generic.SortedDictionary&lt;string,int&gt;.ValueCollection.Enumerator 类型)。

发生的情况是,每次将结构传递给 Print() 方法时,都会复制该结构,因此该方法始终使用原始枚举器的副本 - 这会导致工作异常。

以下程序演示了这一点:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Example();
    }

    public static void Example()
    {
        var sorted = new SortedDictionary<string, int>
        {
            {"1", 1 },
            {"3", 3 },
            {"0", 0 },
            {"2", 2 },
            {"4", 4 }
        };

        var fromValues1 = sorted.Values.GetEnumerator();
        // fromValue1 type is struct System.Collections.Generic.SortedDictionary<string,int>.ValueCollection.Enumerator
        fromValues1.MoveNext();

        while (printNextValue(fromValues1)) // Prints 0 once for each value in the dictionary.
            ;

        Console.WriteLine("-----------------");

        IEnumerator<int> fromValues2 = sorted.Values.GetEnumerator();
        // fromValues2 type is boxed struct System.Collections.Generic.SortedDictionary<string,int>.ValueCollection.Enumerator
        fromValues2.MoveNext();

        while (printNextValue(fromValues2)) // Prints each value in the dictionary.
            ;
    }

    static bool printNextValue(IEnumerator<int> enumerator)
    {
        Console.WriteLine(enumerator.Current);
        return enumerator.MoveNext();
    }
}

第一个循环输出全零,而第二个循环输出正确的值。

本例中两个循环的唯一区别是第一个迭代器被声明为:

var fromValues1 = sorted.Values.GetEnumerator();

第二个声明为:

IEnumerator&lt;int&gt; fromValues2 = sorted.Values.GetEnumerator();.

第一个声明将导致fromValues1 是一个结构,而第二个是一个装箱结构。

因为这个结构是装箱的,这意味着它在传递给printNextValue() 时不会被复制,这意味着printNextValue() 将使用原始枚举器而不是它的副本。

但是,这并不能解释循环终止的原因!如果每次调用 printNextValue() 时都会复制原始枚举数位置,则循环将永远不会终止,因为原始枚举数的位置永远不会被更新。

这让我相信SortedDictionary.Enumerator 的实现中的一些复杂性意味着当结构被复制时,它的一些数据被复制,但有些不是。

(查看源代码,我怀疑这是由于枚举器实现的 Current 是一个被复制的值类型,但 MoveNext() 似乎操纵了一个堆栈,该堆栈 - 作为一个引用类型 - 在枚举器的所有副本。但是,代码太复杂了,我目前有限的时间无法分析...)

【讨论】:

  • 我猜,这是因为SortedDictionary&lt;TKey, TValue&gt;.ValueCollection.Enumerator 使用了SortedDictionary&lt;TKey, TValue&gt;.Enumerator dictEnum;,而SortedDictionary&lt;TKey, TValue&gt;.Enumerator dictEnum; 使用了TreeSet&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;.Enumerator treeEnum(太多了:))。 TreeSet enumerator 维护 Stack 的树节点。 MoveNext逻辑复杂
  • 当枚举器以这种方式声明var fromValues1 = sorted.Values.GetEnumerator();时,printNextValue方法的开头Current总是指向枚举中的第一项,即0。当 return enumerator.MoveNext();` 被调用时,Current 被设置为“真实”且正确的位置。因此循环在预期时中断。我想,Enumerator 的内部实现和使用一堆项目是原因
  • 所以基本上:Enumerator 是一种值类型,但它不是很擅长它:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-23
  • 1970-01-01
  • 2022-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多