简体   繁体   中英

Does the GetEnumerator() in c# return a copy or the iterates the original source?

I have a simple GetEnumerator usage.

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

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

I want to update the queue outside of this class.

So, I'm doing:

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

Does the GetEnumerator() returns a copy of the queue, or iterated the original value? So while updating, I update the original?

Thank you:)

It depends on the exact underlying implementation.

As far as I remember, most of the built in do.net containers use the current data, and not a snapshot.

You will likely get an exception if you modify a collection while iterating over it -- this is to protect against exactly this issue.

This is not the case for ConcurrentQueue<T> , as the GetEnumerator method returns a snapshot of the contents of the queue (as of.Net 4.6 - Docs )

The IEnumerator interface does not have a set on the Current property, so you cannot modify the collection this way ( Docs )

Modifying a collection (add, remove, replace elements) when iterating is in general risky, as one should not know how the iterator is implemented.

To add on this, a queue is made to get first element / adding element at the end, but in any case would not allow replacing an element "in the middle".

Here are two approaches that could work:

Approach 1 - Create a new queue with updated elements

Iterate over the original queue and recreate a new collection in the process.

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

This is naturally done in one go by using linq .Select and feed the constructor of Queue with the result IEnumerable:

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

Beware, could be resource consuming. Of course, other implementations are possible, especially if your collection is large.

Approach 2 - Collection of mutable elements

You could use a wrapper class to enable mutation of objects stored:

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

Then you create a private ConcurrentQueue<MyObject> queue = new ConcurrentQueue<MyObject>(); instead.

And now you can mutate the elements, without having to change any reference in the collection itself:

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

In the code above, the references stored by the container have never changed. Their internal state have changed, though.

In the question code, you were actually trying to change an object (string) by another object, which is not clear in the case of queue, and cannot be done through .Current which is readonly. And for some containers it should even be forbidden.

Here's some test code to see if I can modify the ConcurrentQueue<string> while it is iterating.

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

That runs successfully and produces abcabcx .

However, if we change the collection to a standard List<string> then it fails.

Here's the implementation:

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

That produces ab before throwing an InvalidOperationException .

For ConcurrentQueue this is specifically addressed by the documentation :

The enumeration represents a moment-in-time snapshot of the contents of the queue. It does not reflect any updates to the collection after GetEnumerator was called. The enumerator is safe to use concurrently with reads from and writes to the queue.

So the answer is: It acts as if it returns a copy. (It doesn't actually make a copy, but the effect is as if it was a copy - ie changing the original collection while enumerating it will not change the items produced by the enumeration.)

This behaviour is NOT guaranteed for other types - for example, attempting to enumerate a List<T> will fail if the list is modified during the enumeration.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM