Back to Blog
C#

C# Collection Expression Array Initialization

c# collection expression array initialization: Learn how to initialize C# arrays with collection expressions, including spread elements, type inference, and compatibil...

C#Collection ExpressionsArray InitializationC# 12
C# collection expression syntax for array initialization showing square brackets and spread operator.

C# 12 introduced collection expressions, a unified syntax for initializing arrays, spans, and other collection types. For array initialization, this replaces the traditional new int[] { ... } with a simpler [ ... ] form. This article explains how c# collection expression array initialization works, where it fits in your code, and what to watch out for.

What Are Collection Expressions?

Collection expressions provide a consistent syntax for creating collections. Instead of writing new int[] { 1, 2, 3 }, you write [1, 2, 3]. The compiler uses the target type to determine the collection type. This works not only for arrays but also for Span<T>, ReadOnlySpan<T>, List<T>, and any type that has a suitable collection initializer pattern.

For arrays, the syntax is straightforward, but the behavior differs from the old syntax in a few important ways. The most notable difference is that collection expressions require a target type. You cannot use var directly with them because there is no type information on the right-hand side.

Basic Array Initialization

The simplest use case is initializing an array with known elements:

int[] numbers = [1, 2, 3, 4, 5];

This is equivalent to the older form:

int[] numbers = new int[] { 1, 2, 3, 4, 5 };

The compiler infers the array element type from the target variable. You can also use collection expressions in method calls where the parameter type is known:

PrintNumbers([1, 2, 3]); void PrintNumbers(int[] numbers) { // ... }

This reduces noise and keeps the call site readable. However, you cannot write var numbers = [1, 2, 3]; because the compiler has no target type to infer from. This is a deliberate design decision to maintain type safety.

Using Spread Elements

One of the most useful features of collection expressions is the spread operator ... It expands an existing collection into a new array. For example:

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

The spread operator works with any enumerable type, including arrays, lists, and even IEnumerable<T>. This is a concise way to concatenate collections without calling Concat or manually copying elements. The compiler generates code that allocates a new array and copies each element from the spread sources.

You can mix literal elements and spread elements:

int[] result = [0, .. first, 10, .. second];

This produces [0, 1, 2, 3, 10, 4, 5, 6]. The order is preserved exactly as written.

Type Inference and Target Typing

Collection expressions rely on target typing. The compiler determines the element type from the context. For example:

string[] names = ["Alice", "Bob"];

If the target type is a Span<T>, the collection expression creates a span over a temporary array. This is efficient for short-lived operations:

Span<int> span = [1, 2, 3];

The compiler may allocate a temporary array to back the span. This behavior is important to understand for performance-sensitive code. If you need a span that does not allocate, consider using stackalloc or a pre-allocated buffer instead.

Target typing also affects how the compiler handles implicit conversions. For instance, if the target type is int[], the element type is int. If the target type is IEnumerable<int>, the compiler may create an array internally and wrap it. This is transparent to the caller but can affect memory usage.

When to Prefer Collection Expressions

Collection expressions are preferable when you want concise, readable code for array initialization. They are especially useful in method arguments, where the target type is known. For example:

ProcessData([1, 2, 3]); void ProcessData(int[] data) { // ... }

This avoids the verbose new int[] syntax and makes the intent clearer. Collection expressions also work well with ReadOnlySpan<T> parameters, which are common in performance-oriented APIs.

However, there are cases where the traditional syntax is still necessary. For instance, if you need to create an array with a specific size but no initial elements, you still use new int[10]. Collection expressions do not support a size specifier. Similarly, for multi-dimensional rectangular arrays, you must use the traditional new int[2,3] { ... } syntax. Collection expressions only support single-dimensional arrays and jagged arrays (arrays of arrays).

Compatibility and Language Version

Collection expressions are available in C# 12 and later. If you are targeting an older language version, you cannot use this syntax. The runtime does not need to be .NET 8; you can use collection expressions with older target frameworks as long as the compiler supports C# 12. However, some features like Span<T> support may require .NET 8 or later. Always check your project's language version setting in the .csproj file:

<PropertyGroup> <LangVersion>12</LangVersion> </PropertyGroup>

If you are working in a codebase that must support older language versions, you cannot use collection expressions. In that case, stick with the traditional syntax or use helper methods.

Performance and Allocation Behavior

Collection expressions do not introduce a performance penalty compared to traditional array initialization. For simple cases, the compiler generates similar IL. For spread elements, the compiler may generate code that copies elements efficiently. However, when using Span<T>, the compiler may allocate a temporary array, which could be a concern in hot paths. In such cases, consider using stackalloc or a pre-allocated buffer if you need to avoid heap allocations.

Another subtle point: collection expressions can cause a hidden allocation when the target type is an interface or a type that requires a wrapper. For example, assigning to IEnumerable<int> might create an array and then wrap it in a List<int> or a custom iterator. The exact behavior depends on the compiler's implementation and the target type. If you are measuring performance, always profile the specific scenario rather than assuming the compiler's behavior.

Common Mistakes and Edge Cases

One common mistake is assuming that var works with collection expressions. It does not. You must have a target type. Another mistake is using the spread operator with a collection that is not enumerable. The compiler will raise an error. Also, be aware that collection expressions cannot be used to create multi-dimensional arrays directly. For jagged arrays, you can nest collection expressions:

int[][] matrix = [[1, 2], [3, 4]];

But for rectangular arrays, you still need the traditional new int[2,2] { ... } syntax.

Another edge case is the interaction with nullable reference types. If the target type is string?[], the collection expression must contain only nullable strings. The compiler enforces this. If you have a collection expression with mixed null and non-null elements, the target type must be nullable.

Finally, remember that collection expressions are not just for arrays. They work with List<T>, HashSet<T>, and custom collection types that implement a collection builder pattern. The same syntax and rules apply, but the allocation behavior may differ. Always test the specific type you are using.

c# collection expression array initialization: Practical Usa | RYUSLOG DEV