Select Page

A collection expression such as [2, 4, 6] supplies elements. The surrounding code supplies the collection type. If a declaration, parameter, return type or cast supplies a suitable target, the compiler can build the collection. If that context is missing or ambiguous, the element values alone do not settle the collection type.

This tutorial follows that rule through runnable examples and two deliberate compiler errors. If you need a refresher on the types themselves, start with C# Station’s introduction to generic collections.

Verified 21 October 2026 using .NET SDK 10.0.402 and C# 14.0 in a pinned, offline container. Collection expressions were introduced in C# 12, supported on .NET 8. The examples were run only in this environment, targeting net10.0 with nullable analysis and warnings treated as errors. Earlier SDKs and language versions were not tested. The language version configuration reference explains how the target framework selects the default. Each successful example is a separate console program. The two examples labeled as failing are separate compilation tests.

Start with an explicit target type

The same bracketed elements can initialize an array, a list or a supported collection interface. The Microsoft collection expression reference lists those conversions. Choose the target according to the operations your caller needs:

using System;
using System.Collections.Generic;

int[] array = [2, 4, 6];
List<int> list = [2, 4, 6];
IReadOnlyList<int> view = [2, 4, 6];
list.Add(8);
Console.WriteLine($"Array length: {array.Length}");
Console.WriteLine($"List count: {list.Count}");
Console.WriteLine($"Read-only count: {view.Count}");

Expected standard output, asserted during verification:

Array length: 3
List count: 4
Read-only count: 3

Here, int[] supplies the array target and List<int> supplies the growable list target. The interface variable exposes indexed, read-only access. Its declared interface does not specify which concrete implementation the compiler must use, so avoid writing code that depends on a particular runtime collection type.

Why a bare var declaration fails

It is tempting to replace the first declaration’s explicit type with var. This changes the information available to the compiler:

var numbers = [2, 4, 6];

This deliberately failing example produces CS9176; it has no standard output.

The element expressions alone do not choose between an array, a list and other eligible targets. A collection expression has no natural type of its own, as described in the C# 12 feature specification. Fix this case by supplying a target, as in the first example. An empty [] needs a target for the same reason.

Parameters and return types also supply targets

A local variable declaration is only one source of context. In this program, Wrap accepts an array, so its argument has an array target. Each expression-bodied static local function supplies its declared return type as the target for its own collection expression. The same target-typing principle applies to ordinary method parameters and return types. Microsoft’s reference examples show a collection expression passed as a method argument and returned from an expression-bodied property.

using System;
using System.Collections.Generic;

Console.WriteLine(string.Join(", ", Wrap(["review", "publish"])));
Console.WriteLine($"Empty count: {Empty().Count}");

static string[] Wrap(string[] labels) => ["start", ..labels, "done"];
static List<string> Empty() => [];

Expected standard output, asserted during verification:

start, review, publish, done
Empty count: 0

The argument becomes a string[]. The body of Wrap creates another array containing the added labels. The empty expression returned by Empty becomes an empty List<string>. For background on ordinary method parameters and return types, see C# Station’s methods lesson.

Spreading integers builds a separate array

A spread element, written with .., contributes the elements of an enumerable source. You can mix it with individual values. The spread element documentation explains the required enumeration support.

using System;
using System.Collections.Generic;

List<int> middle = [2, 3];
int[] result = [1, ..middle, 4];
middle[0] = 99;
Console.WriteLine(string.Join(", ", result));
Console.WriteLine(string.Join(", ", middle));

Expected standard output, asserted during verification:

1, 2, 3, 4
99, 3

The resulting array contains the integer values captured when it was constructed. Replacing an entry in the source list afterward does not replace the corresponding entry in the result. This example concerns integer elements and replacement of list entries; it demonstrates no deep-copy behavior for objects.

An IEnumerable target still materializes the elements

Declaring the target as IEnumerable<int> does not make collection construction deferred. This matters when a spread source performs work during enumeration. The following iterator counts how many values it has yielded:

using System;
using System.Collections.Generic;

int visits = 0;
IEnumerable<int> ReadValues()
{
    visits++;
    yield return 4;
    visits++;
    yield return 5;
}

IEnumerable<int> values = [..ReadValues()];
Console.WriteLine($"After construction: {visits}");
Console.WriteLine(string.Join(", ", values));
Console.WriteLine($"After enumeration: {visits}");

Expected standard output, asserted during verification:

After construction: 2
4, 5
After enumeration: 2

Both yields happen before the first output line. Enumerating values afterward reads the constructed collection and does not run ReadValues again. The reference’s materialization rule applies even to an IEnumerable<T> target. Keep a deferred sequence as a sequence when deferred execution is the behavior you need; spreading it into a collection expression collects its elements.

Make the target explicit when overloads compete

For this array/list overload pair, both overloads accept the expression, but neither conversion is better: the element conversions are identical, both targets are noninterface, non-ref-struct types, and neither target type implicitly converts to the other. The compiler therefore cannot select one overload:

using System;
using System.Collections.Generic;

static class Program
{
    static void Main()
    {
        Show([1, 2, 3]);
    }

    static void Show(int[] values) => Console.WriteLine($"array:{values.Length}");
    static void Show(List<int> values) => Console.WriteLine($"list:{values.Count}");
}

This deliberately failing example produces CS0121; it has no standard output.

The conversion documentation discusses overloaded collection parameters and explicit casts. In this case, cast the expression to the intended array type:

using System;
using System.Collections.Generic;

static class Program
{
    static void Main()
    {
        Show((int[])[1, 2, 3]);
    }

    static void Show(int[] values) => Console.WriteLine($"array:{values.Length}");
    static void Show(List<int> values) => Console.WriteLine($"list:{values.Count}");
}

Expected standard output, asserted during verification:

array:3

The cast supplies an array target, and the array overload is selected. The reference describes tie-breakers that resolve other overload pairs. Here, the explicit array target removes the ambiguity.

A short checklist for reading collection expressions

  • Find the target in the surrounding context, such as a declaration, parameter, return type or cast.
  • Keep the type explicit when a bare var would remove that context.
  • Read ..source as contributing source elements to the constructed collection.
  • Remember that an enumerable interface target still collects all elements during construction.
  • When an overload call is ambiguous, specify the intended target and check the resulting behavior.

The examples establish these typing and materialization behaviors. They do not establish allocation counts or performance advantages; choose the target for its required API and measure performance separately if it matters to your application.

Share This