C# Collection Expression: Syntax and Examples
c# collection expression: Learn how to use C# collection expressions to initialize arrays, lists, spans, and more with concise syntax and clear examples.
C# collection expressions, introduced in C# 12, offer a consistent way to initialize many collection types without relying on type-specific syntax. Instead of using new int[] { ... } or new List<int> { ... }, you can write [1, 2, 3] and let the compiler infer the target type from the context. This reduces verbosity and makes collection initialization more uniform across arrays, spans, lists, and custom collection types.
Basic Syntax and Target Typing
The core syntax is straightforward: a comma-separated list of elements inside square brackets. The target type is determined by the variable declaration, method parameter, or property assignment. For example:
int[] numbers = [1, 2, 3]; List<string> names = ["Alice", "Bob"]; Span<int> span = [4, 5, 6];
In each case, the compiler constructs the appropriate collection type. This works because collection expressions rely on a pattern known as the collection builder. The compiler looks for a suitable Create method or a known construction pattern for the target type.
Creating Different Collection Types
Collection expressions can target any type that satisfies the collection builder requirements. This includes arrays, List<T>, Span<T>, ReadOnlySpan<T>, ImmutableArray<T>, and many other types that implement the necessary pattern. For custom types, you can provide a Create method to enable collection expression support.
For example, to initialize an ImmutableArray<int>:
ImmutableArray<int> values = [10, 20, 30];
This is possible because ImmutableArray<T> has a builder that the compiler can use. The same syntax works for HashSet<T> and Dictionary<K, V> in many cases, though dictionaries require key-value pairs.
Using the Spread Operator
The spread operator .. allows you to include elements from another collection or any enumerable. This is useful when you need to combine existing data with new elements. For instance:
int[] first = [1, 2]; int[] second = [.. first, 3, 4]; // [1, 2, 3, 4]
The spread operator can appear anywhere in the list, and you can spread multiple collections. The compiler handles the iteration and expansion at compile time when possible, or at runtime for dynamic sources.
Empty Collection Expressions
An empty collection expression is simply []. It creates an empty collection of the target type. This can be cleaner than calling Array.Empty<T>() or new List<T>(). For example:
List<int> emptyList = []; int[] emptyArray = [];
This is particularly handy when you need to return an empty collection from a method without allocating a new instance each time, depending on the target type's implementation.
Performance and Allocation Considerations
The performance characteristics of collection expressions depend on the target type. When targeting a Span<T> or ReadOnlySpan<T>, the compiler can often allocate the data on the stack or use static data, avoiding heap allocation entirely. For arrays and lists, the behavior is similar to traditional initializers, but the compiler may optimize the construction.
For example, a collection expression targeting a ReadOnlySpan<int> can be compiled to a static array reference, which means no runtime allocation. This is a significant advantage in hot paths where you need to pass a fixed set of values to a method.
However, if you spread a collection that is not known at compile time, the compiler must generate code to copy elements at runtime, which incurs a loop and possible allocations depending on the target.
Compatibility and Language Version Requirements
Collection expressions are a C# 12 language feature. To use them, you need a compiler that supports C# 12 (Visual Studio 17.8 or later, or .NET 8 SDK). The runtime does not need to be .NET 8 specifically; the feature is purely a compile-time transformation. However, some target types, like ImmutableArray<T>, require the System.Collections.Immutable package, which is available in .NET Core and .NET 5+.
If you are working with older codebases or need to maintain compatibility with C# 11 or earlier, collection expressions are not available. You would need to fall back to traditional initializers.
Common Pitfalls and Edge Cases
One common mistake is assuming that collection expressions always allocate a new collection. For value types like Span<T>, the expression may be stack-allocated, so you cannot return it from a method. Also, when using the spread operator with a collection that implements IEnumerable<T>, the compiler may need to iterate it at runtime, which can affect performance if the source is large.
Another edge case is target type inference. If the target type is not clear, the compiler will raise an error. For example, using var with a collection expression is not allowed because there is no natural type. You must specify the type explicitly.
var invalid = [1, 2]; // error CS9176: There is no target type for the collection expression
Instead, declare the type:
int[] valid = [1, 2];
Collection expressions also work with custom types if you implement the collection builder pattern, but that requires defining a Create method with the appropriate signature. This advanced usage lets you bring the same concise syntax to domain-specific collection types, but it adds a layer of complexity that is unnecessary for most applications.