Collection Expressions in C# 12

C# 12 includes something called Collection Expressions. These offer more generic way to create our collections from array-like syntax.

Let’s look first at the old style creation of an array of integers

var array = new [] { 1, 2, 3 };

This is simple enough array is of type int[]?. This way of creation arrays is not going away, but what if we want to change the array to a different collection then we end up using collection initializers like this

var list = new List<int> { 1, 2, 3 };

There’s nothing much wrong with this, but essentially we’re sort of doing the same thing, just with different syntax.

Collection expressions now allow us to use syntax such as (below) to create our collection regardless of type

int[] array = [1, 2, 3 ];
List<int> list = [1, 2, 3 ];

On the surface this may not seem a big deal, but imagine you’ve a class that accepts an int[] and maybe you change the type to a List, passing the values via the collection expression [] syntax means that part of your code remains unchanged, it just remains as [1, 2, 3].

Along with this we get to use the spread operator .. for example

List<int> list = [1, 2, 3 ];
int[] array = [.. list];

In this example we’ve created a list then basically copied the items to the array, but a spread operator can be used to concatenate values (or collections), such as

int[] array = [-3, -2, -1, 0, .. list];

Creating your own collections to use collection expressions

For many of the original types, such as List<T> the collection expression code is built in. But newer collections and, if we want, our own collection can take advantage of this syntax by following a minimal set of rules.

All we need to do is create our collection type and add the CollectionBuilderAttribute to it like this

[CollectionBuilder(typeof(MyCollection), nameof(MyCollection.Create))]
public class MyCollection<T>
{
   // our code
}

Now this is not going to work, the typeof expects a non-generic type, so we create a simple non-generic version of this class to handle the creation of the generic version. Also notice the CollectionBuilder expects the name of the method to call and expects a method that takes a single parameter of type ReadOnlySpan and returns the collection type, now initialized, like this

public class MyCollection
{
  public static MyCollection<T> Create<T>(ReadOnlySpan<T> items)
  {
     // returns a MyCollection<T>
  }
}

Let’s look at potential bare minimum implementation of this collection type which can be used with the collection expression syntax. Notice we will also need to implement IEnumerable and/or IEnumerable<T>

[CollectionBuilder(typeof(MyCollection), nameof(MyCollection.Create))]
public class MyCollection<T> : IEnumerable<T>
{
  public static readonly MyCollection<T> Empty = new(Array.Empty<T>());

  private readonly List<T> _innerCollection;

  internal MyCollection(T[]? items)
  {
    _innerCollection = items == null ? new List<T>() : [..items];
  }

  public T this[int index] => _innerCollection[index];
  public IEnumerator<T> GetEnumerator() => _innerCollection.GetEnumerator();
  IEnumerator IEnumerable.GetEnumerator() => _innerCollection.GetEnumerator();
}

public class MyCollection
{
  public static MyCollection<T> Create<T>(ReadOnlySpan<T> items)
  {
    return items.IsEmpty ? 
      MyCollection<T>.Empty : 
      new MyCollection<T>(items.ToArray());
  }
}

Ofcourse this is a silly example as we’re not adding anything that the inner List<T> cannot supply, but you get the idea. Now we can use the collection expression syntax on our new collection type

MyCollection<int> collection = [1, 2, 6, 7];