List<T>.Enumerator
“snapshots” list version upon creation by _version = list._version;
. This allows enumerator to halt enumeration when List<T>
changed during it.
This behavior likely originates from need to move what is hidden under foreach
statement into forefront while in obedience to contract (for each).
foreach(var i in numbers)
{
if (i > 3)
numbers.Remove(i);
}
After removal item order is shifted to left. If there is no versioning, then enumeration can continue on the 2nd next item. By this chance versioning seems fine.
In contrast
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
if (i > 3)
numbers.Remove(i);
}
does not have for each contract. Now it is just decision of List<T>.Enumerator
to be exhaustive making sure it is.
Let consult Sort
method
public void Sort() => Sort(0, Count, null);
public void Sort(int index, int count, IComparer<T>? comparer)
{
… // Some code omitted. Code bellow differently formatted.
if (count > 1)
Array.Sort<T>(_items, index, count, comparer);
_version++;
}
I think, I wonder who would ever do
foreach(var i in numbers)
{
if (some condition)
numbers.Sort();
}
Similar applies to Reverse
.
public void Reverse() => Reverse(0, Count);
public void Reverse(int index, int count)
{
… // Some code omitted. Code bellow differently formatted.
if (count > 1)
Array.Reverse(_items, index, count);
_version++;
}
On other hand
foreach(var i in numbers)
{
if (i % 3)
numbers.Reverse();
}
I expect could produce nice series but I am not mathematician.
This count there are some points from which List<T>.Enumerator
can be viewed:
- It is exhaustive as feature and negative impact on
List<T>
and enumerator performance should be consumed as levy. - It is exhaustive as safeguard. Versioning in this case substitutes analyzer function that helps developers to avoid items miss or repeating them during enumeration due items shift. For this point penalties are debatable.
List<T>
exhibits wastrel implementation.
- Versioning addresses truly only cases that causes left/right items shift copy – removal, insertion.
- Version is also handled for
Reverse
,Sort
but what scenario allows for accidental such change during enumeration? I guess none. - Version is handled in methods:
T this [int index] set
– there it can make sense but I doubt anyone would ever expect previous value when it is deliberately replaced at exact index;Add
– adds to end and enumerator perfectly can work with this (relying on size), no need for version;Clear
– affects enumeration but in safe way, check against size is sufficient;EnsureCapacity
– I do not get how this can threat enumeration;Insert
– avoids items repetition;InsertRange
– avoids items miss and repetition,RemoveAll
,RemoveAt
,RemoveRange
– avoids items miss - If so then, in total, this most likely means that some methods could be freed from versioning by virtue of common sense and/or low probability of misuse while for shifting ones this counts still debatable.
Worth noting that _version
is not protected against overflow. That it turn draws back to point 2. If versioning should serve run-time, should it be overflow protected? Likely.
It seems so List<T>
versioning was designed in order to serve development (time) and there could be provided analyzers, checking shifting methods inside loops. Exception cannot go unnoticed, that is for sure. Also shift can occur outside the loop (method call).
SZArrayEnumerator
vsList<T>.Enumerator
performance. One plain enough stackoverflow.com/questions/454916/…. And yes,_version
in sample come back to its initial value,0
.SZGenericArrayEnumerator<T>
withList<T>.Enumerator
you get that they differ only in versioning logic. There’s difference also inCurrent
but these could go same.