5

During my investigations of List enumeration I noticed List<T>.Enumerator is struct. In opposite for instance to System.Array.SZArrayEnumerator or System.SZArrayHelper.SZGenericArrayEnumerator<T> that are class.

It seems this not typical usage of struct since enumerator has no traits of value type. For instance it modifies its state, it is never passed "by value" anywhere, etc.

What is design intention here?

  • 5
    Fewer allocations means less memory pressure for the garbage collector. As Net Core and C# continue to evolve, more things are moving toward struct like ValueTask<T> when you are running a task synchronously. – Berin Loritsch Jun 10 at 14:01
  • 4
    Enumerators are intended to be used in a loop, and then discarded. They are not meant to be passed at all. That makes it an ideal opportunity to optimize the enumerator into a struct. – Berin Loritsch Jun 10 at 14:03
  • 2
    Kudos for looking through the reference source. So few developers do this. – Robert Harvey Jun 10 at 14:06
  • 1
    In game development you might iterate over the same array each frame 120 frames per second. If GetEnumerator would allocate we would see alot of GC spikes – Anders Jun 10 at 16:50
  • 1
    @Ucho every time GetEnumerator gets called (which includes every time you do a foreach over the List<T>) the Enumerator gets instantiated. And yes, you could very easily be doing that every frame in a video game. Although, that is on a List<T>, on an array as Andres says, the foreach would be optimized into indexed access. – Theraot Jun 10 at 23:19
3

According to a video from a conference session called "Writing Allocation Free Code In C#", the Net Core team is taking efforts to reduce unnecessary object allocations. Since struct is a "Value" type, it exists on the stack and is passed by copy. That is in opposition to the class which is a "Reference" type which exists in heap memory. There are a couple reasons to move in this direction:

  • Performance as stack allocation and clean up of small objects is faster than allocating and deallocating heap objects.
  • Runtime stability, not in the sense of things breaking, but more in the sense of reducing garbage collection pauses.

Some of the areas where this can make a big difference include the following:

  • Mobile gaming
  • Internet of Things devices (constrained runtime)

That said, it is very difficult to get the right application behavior using value types when most everything you use is a reference type. These changes are targeted with new APIs and new language features. In select cases (like List<T>.Enumerator) the newer iterations of Net Core will be making those optimizations in areas that they have the best hope of getting right.

I'm hoping the YouTube video stays up indefinitely, since there is a lot of good information on what language features and API changes support value types better.

I will repeat the same caveat that the speaker in the video had: Don't switch to value types in your APIs unless you really need to (for performance, etc.). It is easy to do it wrong, particularly where the struct is maintaining state like an enumerator. That said, the features are there when you need to make use of them.

| improve this answer | |
  • 3
    There's another caveat here: value types are boxed onto the heap if accessed via an interface, so you'd still incur a heap allocation if enumerating via IList<T> or IEnumerable<T>. You'd have to be holding onto a concrete List<T> instance to avoid the allocation. – casablanca Jun 11 at 3:32
  • Correct. Bottom line: easy to get wrong, hard to get right. There be dragons. – Berin Loritsch Jun 11 at 11:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.