.net c#中的GetEnumerator()是返回副本还是迭代原始源代码?

pgccezyw  于 2023-01-06  发布在  .NET
关注(0)|答案(4)|浏览(117)

我有一个简单的GetEnumerator用法。

private ConcurrentQueue<string> queue = new ConcurrentQueue<string>();

public IEnumerator GetEnumerator()
{
    return queue.GetEnumerator();
}

我想更新这个类之外的队列。
所以,我在做:

var list = _queue.GetEnumerator();
while (list.MoveNext())
{
    list.Current as string = "aaa";
}

**GetEnumerator()是返回队列的副本,还是迭代原始值?**所以在更新时,我更新原始值?

谢谢你:)

5rgfhyps

5rgfhyps1#

这取决于确切的底层实现。
据我所知,大多数内置的dotnet容器使用当前数据,而不是快照。
如果在迭代集合时修改集合,则很可能会遇到异常--这正是为了防止这个问题。
ConcurrentQueue<T>的情况并非如此,因为GetEnumerator方法返回队列内容的快照(从.Net 4.6 - Docs开始)
IEnumerator接口的Current属性上没有set,因此不能以这种方式修改集合(Docs)

nwlls2ji

nwlls2ji2#

在迭代时修改集合(添加、删除、替换元素)通常是有风险的,因为人们不应该知道迭代器是如何实现的。
为了在此基础上添加,创建一个队列以获得第一个元素/在末尾添加元素,但在任何情况下都不允许替换“中间”的元素。
以下是两种可行的方法:

方法1 -使用更新的元素创建新队列

迭代原始队列并在进程中重新创建一个新集合。

var newQueueUpdated = new ConcurrentQueue<string>();
var iterator = _queue.GetEnumerator();
while (iterator.MoveNext())
{
    newQueueUpdated.Add("aaa");
}
_queue = newQueueUpdated;

通过使用linq .Select并将结果IEnumerable提供给Queue的构造函数,可以自然地一次完成:

_queue  = new ConcurrentQueue<string>(_queue.Select(x => "aaa"));

注意,可能会消耗资源。当然,其他实现也是可能的,特别是当你的集合很大的时候。

方法2 -可变元素的集合

您可以使用 Package 器类来启用所存储对象的变异:

public class MyObject
{
    public string Value { get; set; }
}

然后创建一个private ConcurrentQueue<MyObject> queue = new ConcurrentQueue<MyObject>();
现在,您可以改变元素,而不必更改集合本身中的任何引用:

var enumerator = _queue.GetEnumerator();
while (enumerator.MoveNext())
{
    enumerator.Current.Value = "aaa";
}

在上面的代码中,容器存储的引用从未改变,但是它们的 * 内部状态 * 改变了。
在问题代码中,你实际上是试图用另一个对象来改变一个对象(字符串),这在队列的情况下是不清楚的,并且不能通过只读的.Current来完成,对于一些容器甚至应该禁止。

icnyk63a

icnyk63a3#

下面是一些测试代码,看看我是否可以在ConcurrentQueue<string>迭代时对其进行修改。

ConcurrentQueue<string> queue = new ConcurrentQueue<string>(new[] { "a", "b", "c" });

var e = queue.GetEnumerator();

while (e.MoveNext())
{
    Console.Write(e.Current);
    if (e.Current == "b")
    {
        queue.Enqueue("x");
    }
}

e = queue.GetEnumerator(); //e.Reset(); is not supported
while (e.MoveNext())
{
    Console.Write(e.Current);
}

它成功运行并生成abcabcx
但是,如果我们将集合更改为标准的List<string>,则会失败。
实现方法如下:

List<string> list = new List<string>(new[] { "a", "b", "c" });

var e = list.GetEnumerator();

while (e.MoveNext())
{
    Console.Write(e.Current);
    if (e.Current == "b")
    {
        list.Add("x");
    }
}

e = list.GetEnumerator();
while (e.MoveNext())
{
    Console.Write(e.Current);
}

它在引发InvalidOperationException之前生成ab

qv7cva1a

qv7cva1a4#

对于ConcurrentQueue,则为specifically addressed by the documentation
枚举表示队列内容的即时快照。它不反映调用GetEnumerator后对集合的任何更新。枚举数可安全地同时用于读取和写入队列。
所以答案是:它的行为就像它返回了一个副本。(它实际上并没有创建副本,但效果就像它是一个副本一样--即在枚举原始集合的同时改变它不会改变枚举产生的项。)
其他类型不能保证此行为-例如,如果列表在枚举期间被修改,则尝试枚举List<T>将失败。

相关问题