List<> struct Enumerator vs array enumerator
why List<> struct Enumerator holds current value instead of just index to that value like in array enumerator?
73 Replies
https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs,9c3d580a8b7a8fe8,references
List<> struct Enumerator also hold index
but why then it holds index and current value instead of just index?
idk, probably it is newer?
most of the
IEnumerator implementation now caches Current because it may not be O(1) get from its data structurebut accessing item by index in array is constant time, why cache
Current ?mb, the words are not good, time complexity is unrelated here
i mean keep returning
Current by _list[_index] maybe slightly slower than caching it at the first (in MoveNext) and keep returning the same _current for subsequent accessmake sense when same
Current accessed multiple times, but are there any other cases when it makes sense?From IEnumerator<T>:
// Returns the current element of the enumeration. The returned value is
// undefined before the first call to MoveNext and following a
// call to MoveNext that returned false. Multiple calls to
// GetCurrent with no intervening calls to MoveNext
// will return the same object.
new T Current { get; }
Maybe because they don't want a different behavior on calling Current depending on the underlying data structure?
If index is 0 by default and it's just a property forwarding to the inner list.
It has a different meaning than the IEnumerator<T>.Current is supposed to mean.
If it's -1, it's gonna throw. And they might want not to have to deal with a if case on the Current property. So caching is better than 1 comparison every access
ArrayEnumerator index started from -1, there should be no difference as long as calling Current causes undefined behaviour before MoveNextCalling Current with index -1 is gonna crash no?
on everything else, it's unititialized memory no?
Better optimising not to have to check for -1 i guess
ティナ
REPL Result: Failure
Exception: InvalidOperationException
Compile: 283.531ms | Execution: 21.220ms | React with ❌ to remove this embed.
hum, beside multiple access, no clue then. :_SuwonShrug:
Didn't know that was gonna throw lol
Thanks
welp it checks the index
but not for List?
but not for list
they just assign default to list iirc
:aussmie:
hold on, could you try the same code with list
does it throw too?
oh nullable
throw btw
its
_index start from 0ティナ
REPL Result: Failure
Exception: InvalidOperationException
Compile: 355.219ms | Execution: 22.120ms | React with ❌ to remove this embed.
what about the public T Current
?
I'm lost rn X)
generic Current should'n throw exception
How do you access it differently?
IEnumerator<int> e=a.GetEnumerator();
the generic one doesnt throw i guess
ティナ
REPL Result: Success
Result: int
Compile: 294.050ms | Execution: 21.493ms | React with ❌ to remove this embed.
Interresting
:email:
Same with array i guess
Array class doesnt implement the generice version, it just implements IListティナ
REPL Result: Failure
Exception: CompilationErrorException
Compile: 352.310ms | Execution: 0.000ms | React with ❌ to remove this embed.
thats pretty old
any reason why?
array doesn't have generic enumerator
reason why i interested in implementation of Enumerator, i'm writing own PooledArray and need to make decision how to implement enumerator

caching current value leads to bigger heap allocations in case then Enumerator boxes to interface, especially for large structs
Part of why you asked why the list enumerator caches it I Believe
exactly
No way to know who wrote the List version?
:KEK:
one sec
list has a method returning the struct directly iirc
Because maybe there is some good reasons. Maybe not :kappa:
It's funny to see that some things are implemented only on half the things lol

Still better to have them around even little by little though
List<T>.GetEnumerator Method (System.Collections.Generic)
Returns an enumerator that iterates through the List<T>.
Stephen?
the one that return the struct directly
looks like i should watch all videos about IEnumerable with Stephen Toub
https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerator-1?view=net-9.0
Even this example shows caching the value
IEnumerator<T> Interface (System.Collections.Generic)
Supports a simple iteration over a generic collection.
If you get the Reasons behind it.
It would be cool to let us know here!
i plan to watch videos with Stephen about IEnumerable, i'll let you know if i find anything interesting
you can have a simple
IEnumerator for all IReadOnlyList (but it cannot detect modification), make sense to cache current cuz you dont know if the indexer is O(1)it's make sense if you don't know about indexer implementation
But what if you do it when you know it's O(1)?
Maybe it's about having the same size for all IEnumerator<T> for some compiler trick somewhere?
i don't think
or reinterpret cast or whatever?
Oh well that wouldn't make sence given value types
i see another reason to cache current
if you cache Current you ensure what it cannot be changed ( If value type, or cannot be assigned other reference for ref type )
List Enumerator have also version check
ooh

No deep immutability hits
they tends to throw if the version not match
Mystery solved then
but i think that wouldnt happen in concurrent collection
don't they freeze the Stuff in a new array?
Saw that for concurrent bag last time I checked. I was kinda disappointed
Maybe it's not the case. or not anymore. But I'm fairly sure it copies the content as a Snapshot for current values when doing the enumerator.
It just locks while doing the copy.
If it wasn't cached (in Current), it would have to check the version every time the Current property is called, since the underlying array could have been changed (due to List expansion)
Sorry if you have already figured this out, was too lazy to read all the conversation
probably it's cached to prevent version check on every Current access, but i'm not sure
interesting, when you use indexer on List<> to set value it increments version
but
if you use CollectionsMarshal.AsSpan(list) and sets value by indexer
versions doesn't change
the setter of list is skipped
doesn't throw

throws

confused behavior
AsSpan get the internal array of list so you can directly set the element to the array without using the indexer of list
Yeah, you're kind of «on your own» when working with the underlying array. Just like when the span keeps referring to the same old array even if the List expands and resizes itself
collection marshal
X) not SafeMarchal
look at ArraySegment<T> Enumerator, it doesn't cache Current
So it's increasing memory use so less instructions are made
Oh, I forgot about that