这是因为SortedSet<T>.Enumerator is a struct。每次使用字典的索引器检索枚举器时,都会得到它的新副本。因此,即使您在该副本上调用 MoveNext(),下次您获得枚举数的副本时,它对于 Current 没有任何价值。
有趣的是,由于the exact implementation of that struct 中的一个怪癖,枚举数的每个副本都获得相同的引用类型对象来跟踪枚举的状态(堆栈),因此MoveNext() 方法似乎有效(即它在您第一次调用它时返回true,但在以后的任何时候返回false。
至少有四个选项可以正确处理这个问题……
将副本检索到变量中,并使用变量而不是字典:
var set = new SortedSet<ClassA>();
set.Add(new ClassA());
var map = new Dictionary<int, SortedSet<ClassA>.Enumerator>();
map[1] = set.GetEnumerator();
var e = map[1];
e.MoveNext();
val = e.Current;
请注意,在此示例中,枚举数的字典副本仍然没有您想要的 Current 值。在调用 MoveNext() 后,您必须重新设置字典的副本以保留:map[1] = e;
使用数组代替字典:
var set = new SortedSet<ClassA>();
set.Add(new ClassA());
var map = new SortedSet<ClassA>.Enumerator[2];
map[1] = set.GetEnumerator();
map[1].MoveNext();
var val = map[1].Current;
除了map 的声明和初始化方面的不同之外,这与您现在拥有的代码完全一样。这是因为数组的索引元素是变量,而不是像索引语法对任何其他集合一样通过索引器。因此,您是直接对存储在数组中的枚举数副本进行操作,而不是索引器返回的新副本。
当然,这只有在键值受到足够的约束以使得分配一个足够大的数组来容纳键的所有可能性时才可行。
声明一个引用类型包装器以包含值类型枚举器:
class E<T>
{
public SortedSet<T>.Enumerator Enumerator;
public E(SortedSet<T>.Enumerator enumerator)
{
Enumerator = enumerator;
}
}
那么……
var set = new SortedSet<ClassA>();
set.Add(new ClassA());
var map = new Dictionary<int, E<ClassA>>();
map[1] = new E<ClassA>(set.GetEnumerator());
map[1].Enumerator.MoveNext();
val = map[1].Enumerator.Current;
在这个例子中,字典只是返回对包装器对象的引用,确保枚举器只有一个副本(它存储在包装器对象中,而不是字典中)。因此,每次通过字典访问对象时,都会得到相同的副本。
当然,您最终不得不通过包装器的Enumerator 字段。这有点笨拙。但它会起作用。
将枚举数存储在数组中,但通过字典进行索引:
var set = new SortedSet<ClassA>();
set.Add(new ClassA());
var map = new Dictionary<int, int>();
var a = new SortedSet<ClassA>.Enumerator[1];
map[1] = 0;
a[map[1]] = set.GetEnumerator();
a[map[1]].MoveNext();
val = a[map[1]].Current;
这混合了前面的两个选项。数组用于存储实际的枚举数,因此您可以将它们作为变量来寻址,但字典用作数组的间接级别,因此您可以通过任何您喜欢的键来引用枚举数。
显然,在实际应用程序中,您将通过枚举原始集合、将它们的枚举数存储在数组中并将原始键映射到字典中的数组索引来初始化数组。
额外的间接性有点笨拙,就像在包装器选项中一样,但没有那么糟糕,并且解决了基于数组的选项的数组大小问题。
可以说,您的问题与以下问题重复:List.All(e => e.MoveNext()) doesn't move my enumerators on
肯定和那个密切相关,还有这个:Details on what happens when a struct implements an interface