Edit

Share via


Collection expressions - C# language reference

Use a collection expression to create common collection values. A collection expression is a terse syntax that you can assign to many different collection types. A collection expression contains a sequence of elements between [ and ] brackets.

The C# language reference documents the most recently released version of the C# language. It also contains initial documentation for features in public previews for the upcoming language release.

The documentation identifies any feature first introduced in the last three versions of the language or in current public previews.

Tip

To find when a feature was first introduced in C#, consult the article on the C# language version history.

The following example declares a System.Span<T> of string elements and initializes them to the days of the week:

Span<string> weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
foreach (var day in weekDays)
{
    Console.WriteLine(day);
}

You can convert a collection expression to many different collection types. The first example demonstrated how to initialize a variable by using a collection expression. The following code shows many of the other locations where you can use a collection expression:

// Initialize private field:
private static readonly ImmutableArray<string> _months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

// property with expression body:
public IEnumerable<int> MaxDays =>
    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

public int Sum(IEnumerable<int> values) =>
    values.Sum();

public void Example()
{
    // As a parameter:
    int sum = Sum([1, 2, 3, 4, 5]);
}

You can't use a collection expression where a compile-time constant is expected, such as when initializing a constant, or as the default value for a method argument.

Both of the previous examples used constants as the elements of a collection expression. You can also use variables for the elements, as shown in the following example:

string hydrogen = "H";
string helium = "He";
string lithium = "Li";
string beryllium = "Be";
string boron = "B";
string carbon = "C";
string nitrogen = "N";
string oxygen = "O";
string fluorine = "F";
string neon = "Ne";
string[] elements = [hydrogen, helium, lithium, beryllium, boron, carbon, nitrogen, oxygen, fluorine, neon];
foreach (var element in elements)
{
    Console.WriteLine(element);
}

Spread element

Use a spread element .. to inline collection values in a collection expression. The following example creates a collection for the full alphabet by combining a collection of the vowels, a collection of the consonants, and the letter "y", which can be either:

string[] vowels = ["a", "e", "i", "o", "u"];
string[] consonants = ["b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
                       "n", "p", "q", "r", "s", "t", "v", "w", "x", "z"];
string[] alphabet = [.. vowels, .. consonants, "y"];

The spread element ..vowels, when evaluated, produces five elements: "a", "e", "i", "o", and "u". The spread element ..consonants produces 20 elements, the number in the consonants array. The expression in a spread element must be enumerable by using a foreach statement. As shown in the previous example, you can combine spread elements with individual elements in a collection expression.

Conversions

You can convert a collection expression to different collection types, including:

Note

You can't use collection expressions to initialize inline arrays. Inline arrays require different initialization syntax.

Important

A collection expression always creates a collection that includes all elements in the collection expression, regardless of the target type of the conversion. For example, when the target of the conversion is System.Collections.Generic.IEnumerable<T>, the generated code evaluates the collection expression and stores the results in an in-memory collection.

This behavior is distinct from LINQ, where a sequence might not be instantiated until it is enumerated. You can't use collection expressions to generate an infinite sequence that won't be enumerated.

The compiler uses static analysis to determine the most performant way to create the collection declared with a collection expression. For example, the empty collection expression, [], can be realized as Array.Empty<T>() if the target won't be modified after initialization. When the target is a System.Span<T> or System.ReadOnlySpan<T>, the storage might be stack allocated. The collection expressions feature specification specifies the rules the compiler must follow.

Many APIs are overloaded with multiple collection types as parameters. Because a collection expression can be converted to many different expression types, these APIs might require casts on the collection expression to specify the correct conversion. The following conversion rules resolve some of the ambiguities:

  • A better element conversion is preferred over a better collection type conversion. In other words, the type of elements in the collection expression has more importance than the type of the collection. These rules are described in the feature spec for better conversion from collection expression.
  • Conversion to Span<T>, ReadOnlySpan<T>, or another ref struct type is better than a conversion to a non-ref struct type.
  • Conversion to a noninterface type is better than a conversion to an interface type.

When you convert a collection expression to a Span or ReadOnlySpan, the span object's safe context comes from the safe context of all elements included in the span. For detailed rules, see the Collection expression specification.

Collection builder

Collection expressions work with any collection type that is well-behaved. A well-behaved collection has the following characteristics:

  • The value of Count or Length on a countable collection produces the same value as the number of elements when enumerated.
  • The types in the System.Collections.Generic namespace are side-effect free. The compiler can optimize scenarios where these types might be used as intermediary values, but it doesn't expose them otherwise.
  • A call to an applicable .AddRange(x) member on a collection results in the same final value as iterating over x and adding all of its enumerated values individually to the collection by using .Add.

All the collection types in the .NET runtime are well-behaved.

Warning

If a custom collection type isn't well-behaved, the behavior is undefined when you use that collection type with collection expressions.

Your types opt in to collection expression support by writing a Create() method and applying the System.Runtime.CompilerServices.CollectionBuilderAttribute attribute on the collection type to indicate the builder method. For example, consider an application that uses fixed length buffers of 80 characters. That class might look something like the following code:

public class LineBuffer : IEnumerable<char>
{
    private readonly char[] _buffer;
    private readonly int _count;

    public LineBuffer(ReadOnlySpan<char> buffer)
    {
        _buffer = new char[buffer.Length];
        _count = buffer.Length;
        for (int i = 0; i < _count; i++)
        {
            _buffer[i] = buffer[i];
        }
    }

    public int Count => _count;
    
    public char this[int index]
    {
        get
        {
            if (index >= _count)
                throw new IndexOutOfRangeException();
            return _buffer[index];
        }
    }

    public IEnumerator<char> GetEnumerator()
    {
        for (int i = 0; i < _count; i++)
        {
            yield return _buffer[i];
        }
    }
    
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    // etc
}

You want to use it with collection expressions as shown in the following sample:

LineBuffer line = ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!'];

The LineBuffer type implements IEnumerable<char>, so the compiler recognizes it as a collection of char items. The type parameter of the implemented System.Collections.Generic.IEnumerable<T> interface indicates the element type. You need to make two additions to your application to be able to assign collection expressions to a LineBuffer object. First, you need to create a class that contains a Create method:

internal static class LineBufferBuilder
{
    internal static LineBuffer Create(ReadOnlySpan<char> values) => new LineBuffer(values);
}

The Create method must return a LineBuffer object, and it must take a final parameter of the type ReadOnlySpan<char>. The type parameter of the ReadOnlySpan must match the element type of the collection. A builder method that returns a generic collection has the generic ReadOnlySpan<T> as its parameter. The method must be accessible and static.

Starting in C# 15, the Create method can have additional parameters before the ReadOnlySpan<T> parameter. You can pass values to these parameters by using a with(...) element in the collection expression. See Collection builder arguments for details.

Finally, you must add the CollectionBuilderAttribute to the LineBuffer class declaration:

[CollectionBuilder(typeof(LineBufferBuilder), "Create")]

The first parameter provides the name of the Builder class. The second attribute provides the name of the builder method.

Collection expression arguments

Starting in C# 15, you can pass arguments to the underlying collection's constructor or factory method by using a with(...) element as the first element in a collection expression. This feature enables you to specify capacity, comparers, or other constructor parameters directly within the collection expression syntax. For more information, see the collection expression arguments feature specification.

The with(...) element must be the first element in the collection expression. The arguments declared in the with(...) element are passed to the appropriate constructor or create method based on the target type. You can use any valid expression for the arguments in the with element.

Constructor arguments

When the target type is a class or struct that implements System.Collections.IEnumerable, the arguments in with(...) are evaluated and the results are passed to the constructor. The compiler uses overload resolution to select the best matching constructor:

public void CollectionArgumentsExamples()
{
    string[] values = ["one", "two", "three"];

    // Pass capacity argument to List<T> constructor
    List<string> names = [with(capacity: values.Length * 2), .. values];

    // Pass comparer argument to HashSet<T> constructor
    HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO", "hello"];
    // set contains only one element because all strings are equal with OrdinalIgnoreCase

    // Pass capacity to IList<T> (uses List<T> constructor)
    IList<int> numbers = [with(capacity: 100), 1, 2, 3];
}

In the preceding example:

Collection builder arguments

For types with a System.Runtime.CompilerServices.CollectionBuilderAttribute, the arguments declared in the with(...) element are evaluated and the results are passed to the create method before the ReadOnlySpan<T> parameter. This feature allows create methods to accept configuration parameters:

internal static class MySetBuilder
{
    internal static MySet<T> Create<T>(ReadOnlySpan<T> items) => new MySet<T>(items);
    internal static MySet<T> Create<T>(IEqualityComparer<T> comparer, ReadOnlySpan<T> items) => 
        new MySet<T>(items, comparer);
}

You can then use the with(...) element to pass the comparer:

public void CollectionBuilderArgumentsExample()
{
    // Pass comparer to a type with CollectionBuilder attribute
    // The comparer argument is passed before the ReadOnlySpan<T> parameter
    MySet<string> mySet = [with(StringComparer.OrdinalIgnoreCase), "A", "a", "B"];
    // mySet contains only two elements: "A" and "B"
}

The create method is selected using overload resolution based on the arguments provided. The ReadOnlySpan<T> containing the collection elements is always the last parameter.

Interface target types

Several interface target types support collection expression arguments. The following table shows the supported interfaces and their applicable constructor signatures:

Interface Supported with elements
IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T> () (empty only)
ICollection<T>, IList<T> (), (int capacity)

For IList<T> and ICollection<T>, the compiler uses a System.Collections.Generic.List<T> with the specified constructor.

Restrictions

The with(...) element has the following restrictions:

  • It must be the first element in the collection expression.
  • Arguments can't have dynamic type.
  • It's not supported for arrays or span types (Span<T>, ReadOnlySpan<T>).