I was checking (just being curious) how collection expression syntax, introduced in C# 12, gets translated into IL code, and specifically what exact data types are used under the hood.
Most cases were pretty straightforward:
Collection<int> a = [5, 2];
IEnumerable<int> b = [5, 2];
IReadOnlyList<int> c = [5, 2];
gets translated to:
Collection<int> a = new Collection<int> { 5, 2 };
IEnumerable<int> b = new <>z__ReadOnlyArray<int>(new int[2] { 5, 2 });
IReadOnlyList<int> c = new <>z__ReadOnlyArray<int>(new int[2] { 5, 2 });
Nothing surprising here. The next one, however, puzzles me.
ICollection<int> d = [5, 2];
gets translated, in debug mode, to:
List<int> list = new List<int>();
CollectionsMarshal.SetCount(list, 2);
Span<int> span = CollectionsMarshal.AsSpan(list);
int num = 0;
span[num] = 5;
num++;
span[num] = 2;
num++;
ICollection<int> d = list;
In release mode, the only thing that changes is the removal of d
variable—the list
variable is used directly where d
was used in the original code.
A rough and absolutely non-scientific benchmark of the two approaches shows the following results, when constructing the collection 100,000,000 times:
Number of items in the array | Duration with IEnumerable |
Duration with ICollection |
---|---|---|
2 | 1.0 s. | 1.5 s. |
25 | 7.4 s. | 4.1 s. |
32 | 7.4 s. | 5.0 s. |
It seems that both approaches are comparable in terms of performance: the first one is better when there is a small number of elements, whereas the second one outperforms it when the number of elements start to be higher.
However, the compiler seems to pick an approach based on a type, rather than on a number of actual values in the collection.
What could be the reasons to have this second approach at all, given that the compiler-generated type <>z__ReadOnlyArray`1'<!T>
does implement ICollection<T>
anyway?
ICollection<T>
is mutable and has methods like Add
, so it makes more sense for the compiler to also create an instance of a mutable collection type, as opposed to a read-only array and have it throw NotSupportedException
when you try to Add
.
Compare this to when the type is IReadOnlyCollection<T>
, the compiler does use a read-only array in this case.
IReadOnlyCollection<int> x = [1,2,3];
// compiles to
IReadOnlyCollection<int> readOnlyCollection = new <>z__ReadOnlyArray<int>(array);
See also my answer to this question.