11

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

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?

9
  • 6
    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.CommentedJun 10, 2020 at 14:01
  • 5
    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.CommentedJun 10, 2020 at 14:03
  • 3
    Kudos for looking through the reference source. So few developers do this.CommentedJun 10, 2020 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
    CommentedJun 10, 2020 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
    CommentedJun 10, 2020 at 23:19

1 Answer 1

8

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.

2
  • 4
    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.CommentedJun 11, 2020 at 3:32
  • Correct. Bottom line: easy to get wrong, hard to get right. There be dragons.CommentedJun 11, 2020 at 11:46

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.