Search code examples
c#syntactic-sugarc#-12.0

Why is ICollection<T> treated differently in collection expression syntax?


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?


Solution

  • 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.