C# Collection Expression List Initialization
c# collection expression list initialization: Learn how C# 12 collection expressions simplify list initialization with concise syntax, target typing, and the spread op...
C# 12 collection expressions replace the older initializer syntax for lists and other collection types. For c# collection expression list initialization, the change is straightforward: List<int> numbers = [1, 2, 3]; instead of new List<int> { 1, 2, 3 };. The new syntax is shorter, works across multiple collection types, and the compiler handles the underlying construction.
Collection Expression Syntax for Lists
The basic form of a collection expression is a comma-separated list of elements inside square brackets:
List<int> numbers = [1, 2, 3]; List<string> names = ["Alice", "Bob", "Carol"];
The compiler infers the target type from the variable declaration or the method parameter. You do not need to write new List<int> or specify a capacity. The expression [1, 2, 3] is target-typed, meaning the compiler decides what concrete collection to create based on where the expression appears.
The same syntax works for arrays and other collection types:
int[] array = [1, 2, 3]; Span<int> span = [1, 2, 3];
For List<T>, the compiler generates code equivalent to creating a list and adding each element. For arrays, it creates the array directly. For Span<T>, it can use stackalloc when the span stays on the stack, which avoids heap allocation entirely.
How Target Typing Determines the Result
Collection expressions rely on target typing. The expression [1, 2, 3] has no intrinsic type of its own; the compiler determines the type from the context.
List<int> list = [1, 2, 3]; // List<int> int[] array = [1, 2, 3]; // int[] IEnumerable<int> enumerable = [1, 2, 3]; // List<int> behind the interface
When the target is an interface such as IEnumerable<T> or IReadOnlyList<T>, the compiler picks a concrete implementation, typically List<T>. When the target is a concrete type like List<T> or int[], it uses that type directly.
The same behavior applies to method arguments:
void Process(List<int> values) { /* ... */ } Process([1, 2, 3]);
The compiler creates a List<int> and passes it to the method. If the method accepted int[], the same expression would produce an array instead.
Using the Spread Operator to Combine Collections
The spread operator .. expands an existing collection into the new collection. This is useful when you need to combine several collections into one.
List<int> first = [1, 2]; List<int> second = [3, 4]; List<int> combined = [.. first, .. second, 5];
The result is [1, 2, 3, 4, 5]. The spread operator works with any type that implements IEnumerable<T>, including arrays, lists, and other collection expressions.
int[] left = [1, 2]; int[] right = [3, 4]; List<int> merged = [.. left, .. right];
This is cleaner than the older approach of creating a list and calling AddRange multiple times. The compiler generates the appropriate code to copy each element from the source collection into the target.
What Happens Under the Hood
The compiler translates a collection expression into different code depending on the target type.
For an array target:
int[] numbers = [1, 2, 3];
The compiler emits an array creation with the elements inline, similar to new int[] { 1, 2, 3 }.
For a List<T> target, the compiler creates the list and adds each element. When a spread operator is present, it uses AddRange or an equivalent loop.
For a Span<T> target, the compiler can use stackalloc when the span does not escape the current method. This avoids heap allocation entirely, which matters in hot paths:
Span<int> buffer = [10, 20, 30];
This compiles to a stack-allocated buffer when the span is used only within the method. If the span escapes to a field or a method that stores it, the compiler falls back to an array allocation.
The exact generated code depends on the target type and the compiler version, but collection expressions do not add a runtime abstraction. The compiler resolves the construction at compile time.
When Collection Expressions Fall Short
Collection expressions do not support every collection type. A type must either be a well-known collection type (array, List<T>, Span<T>, IEnumerable<T>, and similar) or have a CollectionBuilderAttribute that points to a builder method.
Custom collection types need explicit builder support:
[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))] public class MyCollection<T> { /* ... */ }
Without the attribute, you cannot use collection expressions with a custom collection type. You would fall back to the traditional constructor and Add calls.
Collection expressions also do not support named or indexed initialization. There is no equivalent to new Dictionary<string, int> { ["key"] = 1 } for dictionaries. Dictionaries and other key-value collections require the older initializer syntax.
Collection expressions cannot be used in every context. For example, you cannot use them as the operand of the is pattern, and some expression-tree scenarios reject them.
Compatibility and Migration Considerations
Collection expressions require C# 12 and a compatible compiler. If your project targets an older language version, the compiler reports an error when it encounters the square-bracket syntax.
When migrating existing code, the change is mostly mechanical:
// Before List<int> numbers = new List<int> { 1, 2, 3 }; // After List<int> numbers = [1, 2, 3];
The behavior is equivalent for List<T>. For arrays, the generated code is also equivalent. For Span<T>, the new syntax can change allocation behavior because the compiler may use stackalloc, so verify that the span usage is compatible with a stack-allocated buffer.
One subtle difference: the older new List<int> { 1, 2, 3 } syntax requires the target type to support collection initializers. Collection expressions have broader target-type support, so code that previously required an explicit type may now compile with a collection expression.
Common Mistakes and Edge Cases
The most common mistake is assuming collection expressions work with any collection type. They do not. If you see a compiler error mentioning collection expressions or collection builders, the target type likely lacks builder support.
Another mistake is using collection expressions with null elements when the target type does not allow them. For a List<string?>, [null] is valid. For a List<string>, it produces a warning or error depending on the nullable context.
The spread operator with an empty source produces no elements, which is usually the desired behavior. But be careful when spreading a collection that could be null:
List<int>? maybeNull = null; List<int> result = [.. maybeNull]; // NullReferenceException at runtime
The spread operator does not guard against null sources. You need to check for null before spreading.
For Span<T> targets, remember that a stack-allocated span cannot be stored in a field or returned from the method. If you need the data to outlive the method, use an array or List<T> instead. Collection expressions with a very large number of elements can also produce a large constructor call, but this is rarely a practical concern because the compiler handles element count efficiently.