C# 12 Collection Expressions and Spread Operator

C# 12 introduces collection expressions and the spread operator, delivering a unified, concise syntax for creating and manipulating collections. This feature eliminates the need for disparate initialization syntaxes across arrays, lists, spans, and immutable types. This article breaks down how collection expressions function, demonstrates the usage of the spread operator, and highlights the performance benefits these additions bring to modern .NET development.

What Are Collection Expressions?

Prior to C# 12, initializing collections required varying syntax depending on the target type:

int[] array = new int[] { 1, 2, 3 };
List<int> list = new List<int> { 1, 2, 3 };
Span<int> span = stackalloc int[] { 1, 2, 3 };

Collection expressions replace these inconsistent declarations with a single, bracketed syntax ([...]):

int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
ReadOnlySpan<int> readOnlySpan = [1, 2, 3];
HashSet<int> set = [1, 2, 3];

Collection expressions are target-typed, meaning the compiler determines the appropriate concrete type based on the context in which the expression appears. You can also pass collection expressions directly into method arguments:

void PrintValues(IEnumerable<string> items) => 
    Console.WriteLine(string.Join(", ", items));

PrintValues(["apple", "banana", "cherry"]);

What Is the Spread Operator?

The spread operator (..) allows you to inline or flatten elements from existing collections directly into a new collection expression. It acts as a cleaner, native alternative to combining collections using methods like Concat() or AddRange().

int[] first = [1, 2, 3];
int[] second = [4, 5, 6];

// Combines elements from both arrays into a single list
List<int> combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]

// Mixes individual elements with spread collections
int[] expanded = [0, ..first, 99, ..second, 100]; 
// [0, 1, 2, 3, 99, 4, 5, 6, 100]

The spread operator works across mixed collection types. For instance, you can spread an array and a List<T> into a ReadOnlySpan<T> seamlessly:

string[] vowels = ["a", "e", "i"];
List<string> remaining = ["o", "u"];

ReadOnlySpan<string> allVowels = [..vowels, ..remaining];

Compiler Optimizations and Performance

Collection expressions are not merely syntactic sugar; they are optimized by the C# compiler:

  • Capacity Allocation: When using the spread operator with collections that implement ICollection<T> or have known lengths, the compiler computes the total required capacity in advance to avoid multiple memory reallocations.
  • Stack Allocation: When targeting Span<T> or ReadOnlySpan<T>, the compiler can allocate the backing storage directly on the stack or reference compiler-generated static data within the assembly's metadata, completely avoiding heap allocations.

Custom Types Support

Collection expressions are extensible. Any custom type can support this syntax by implementing IEnumerable and an Add method, or by decorating the type with the [CollectionBuilder] attribute to specify a custom builder method for initializing the collection.