Skip to main content
SEMastery
.NET Corebeginner

Improve Code Readability with C# Collection Expressions

Learn C# collection expressions and the spread element with simple analogies, diagrams, and code. Write cleaner arrays, lists, and spans in modern .NET.

11 min readUpdated December 12, 2025

Improve Code Readability with C# Collection Expressions

Think about packing a lunchbox for school. You have an empty box, and one by one you add a sandwich, an apple, and a small bottle of water. That works, but it takes time. Now imagine your mother just hands you a box that is already packed and ready. You did not write out every step. You just received a full box.

C# collection expressions are exactly like that ready-packed lunchbox. Instead of writing many lines to build a list step by step, you write the whole thing in one short, clear line. The compiler does the packing for you.

In this post we will learn what collection expressions are, why they make your code easier to read, and how the special spread element (..) lets you mix collections together like pouring two bags of marbles into one.

The old way of making collections

Before C# 12, there were many different ways to create a collection. Each collection type had its own style. Let us look at the common ones.

// An array
int[] numbersArray = new int[] { 1, 2, 3, 4, 5 };
 
// A List
List<string> fruits = new List<string> { "Mango", "Banana", "Guava" };
 
// An array of a known size, then filled by hand
string[] names = new string[3];
names[0] = "Asha";
names[1] = "Ravi";
names[2] = "Priya";

Look closely. Each line repeats the type. The word new appears again and again. For an empty list you had to write new List<string>(). None of this is wrong, but it is noisy. Your eyes have to work harder to find the actual values: the numbers, the fruits, the names. The important things are buried under ceremony.

The new way: collection expressions

C# 12 introduced collection expressions. They use square brackets [ ] and let the compiler figure out the type from the left side. Here is the same code, written the new way.

// An array
int[] numbersArray = [1, 2, 3, 4, 5];
 
// A List
List<string> fruits = ["Mango", "Banana", "Guava"];
 
// An empty collection
List<int> empty = [];

See how clean that is? The brackets hold only the values. There is no repeated type, no new, no extra curly braces. The variable on the left already told the compiler what kind of collection you want, so you do not have to say it twice.

This one bracket style works for arrays, List<T>, Span<T>, ReadOnlySpan<T>, and many other collection types. You learn one shape and use it everywhere.

The same square-bracket syntax targets many collection types

How does the compiler know the type?

This is the clever part. The square brackets by themselves do not have a type. The compiler looks at the target — the place where the value is going — and decides what to build. We call this the target type.

If you write int[] x = [1, 2, 3];, the target is int[], so the compiler makes an array. If you write List<int> x = [1, 2, 3];, the target is a list, so it makes a list. The same [1, 2, 3] becomes a different thing depending on where it lands.

How the compiler chooses the collection type

See []
Read target type
Pick builder
Create collection

Steps

1

See []

Compiler spots the bracket syntax

2

Read target type

Looks at the variable or return type

3

Pick builder

Array, List, Span, etc.

4

Create collection

Builds it the efficient way

The target on the left tells the compiler what to build

Because the type comes from the target, a collection expression cannot stand totally alone with no context. The compiler always needs to know where the value is going. Most of the time that is obvious from the variable, the method return type, or the method parameter.

The spread element: pouring collections together

Now comes the most fun part. The spread element is two dots .. placed in front of a collection inside the brackets. It takes every item out of that collection and drops them into the new one.

Imagine you have one bag of red marbles and one bag of blue marbles. You want a single bag with all the marbles. You just pour both bags into the new bag. The spread element pours collections.

int[] firstHalf = [1, 2, 3];
int[] secondHalf = [4, 5, 6];
 
// Pour both into one new array
int[] all = [..firstHalf, ..secondHalf];
// all is now [1, 2, 3, 4, 5, 6]

The ..firstHalf means "spread out every item of firstHalf right here". You can mix spread items with plain values too.

int[] middle = [10, 20, 30];
 
// Put a 0 at the start, the middle in the centre, and 99 at the end
int[] result = [0, ..middle, 99];
// result is now [0, 10, 20, 30, 99]

This used to take a loop or several AddRange calls. Now it is one readable line that reads almost like a sentence.

The spread element merges two arrays into one

A small but important note. Many people call .. the "spread operator". In C# the correct name is the spread element. There is no separate operator; the .. is part of the collection expression syntax. It is a tiny detail, but using the right name helps when you read the official docs.

A side-by-side comparison

Let us put the old style and the new style next to each other. This table shows how much noise disappears.

TaskOld styleCollection expression
Empty listnew List<int>()[]
Array of numbersnew int[] { 1, 2, 3 }[1, 2, 3]
Fill a listnew List<string> { "a", "b" }["a", "b"]
Merge two arraysa.Concat(b).ToArray()[..a, ..b]
Add item to frontmanual copy or Insert[item, ..rest]

Every line on the right is shorter and easier to read. The values stand out, and the boilerplate is gone.

Where can you use collection expressions?

You can use them in far more places than just variable assignments. Here are the common spots.

PlaceExample
Variableint[] x = [1, 2, 3];
Return valuereturn [1, 2, 3];
Method argumentProcess([1, 2, 3]);
Default field valueprivate int[] _ids = [];
params parameter (C# 14)Sum([1, 2, 3]);

In each case the target type is clear, so the compiler knows what to build. Returning a collection becomes especially clean.

public int[] GetLuckyNumbers()
{
    // The return type is int[], so the compiler builds an array
    return [7, 13, 21];
}
 
public List<string> GetTeam()
{
    // The return type is List<string>, so it builds a list
    return ["Sachin", "Virat", "Rahul"];
}

Working with spans for speed

Sometimes you want a collection that is fast and creates no garbage for the memory cleaner to chase. That is what Span<T> and ReadOnlySpan<T> are for. Collection expressions shine here.

When the target is a span, the compiler can store the items on the stack instead of the heap. The stack is a small, very fast piece of memory that cleans itself up automatically. This means zero heap allocation in many cases.

// The compiler may store these on the stack — no heap allocation
ReadOnlySpan<int> scores = [90, 85, 78, 100];
 
int total = 0;
foreach (int score in scores)
{
    total += score;
}

You write the same friendly [ ] syntax, but you quietly get high performance. You do not have to learn a different style for the fast path.

Spans can live on the fast stack instead of the heap

What changed in C# 14 and .NET 10

Collection expressions started in C# 12 with .NET 8. They kept getting better. In C# 14, which ships with .NET 10 (the current LTS release), they grew in two helpful ways.

First, you can now use collection expressions in even more places, such as yield return, params parameters, and methods that return IEnumerable. The reach of the feature got wider.

Second, params is no longer stuck with only arrays. A method can declare params ReadOnlySpan<int> and avoid the heap allocation that a params array always needed. When you call it, the compiler can build the arguments on the stack. This is a real speed win for methods that take a flexible number of values.

// C# 14: params can be a span, not just an array
public static int Sum(params ReadOnlySpan<int> values)
{
    int total = 0;
    foreach (int v in values)
    {
        total += v;
    }
    return total;
}
 
// Call it like normal — clean and allocation-free
int answer = Sum(1, 2, 3, 4, 5); // 15

How collection expressions grew over time

C# 12 / .NET 8
C# 13 / .NET 9
C# 14 / .NET 10

Steps

1

C# 12 / .NET 8

Brackets and spread element arrive

2

C# 13 / .NET 9

More targets and refinements

3

C# 14 / .NET 10

params spans, wider use, less allocation

The feature widened with each release

A practical example: building a menu

Let us tie it together with a small everyday example. Suppose you run a tea stall app. You have a fixed list of classic items, a list of today's specials, and you always want to show "Water" at the very end. With collection expressions, building the full menu is one line.

string[] classics = ["Masala Chai", "Black Tea", "Green Tea"];
string[] specials = ["Ginger Chai", "Cardamom Chai"];
 
// Combine classics, then specials, then always add Water at the end
string[] fullMenu = [..classics, ..specials, "Water"];
 
foreach (string item in fullMenu)
{
    Console.WriteLine(item);
}
// Masala Chai, Black Tea, Green Tea, Ginger Chai, Cardamom Chai, Water

Read that one line out loud: "the classics, then the specials, then Water". The code matches how you would describe it to a friend. That is what good readability means — the code says what it does, plainly.

A few simple rules to remember

These small tips will keep you out of trouble while you learn.

  • The target type must be clear. var x = [1, 2, 3]; will not compile, because var gives the compiler nothing to aim at. Write the type, like int[] x = [1, 2, 3];.
  • An empty collection is just []. No type needed in the brackets, because the target supplies it.
  • The spread .. goes before the collection you want to pour in, inside the brackets.
  • You can mix spreads and single values freely, like [first, ..middle, last].
  • For the fastest, garbage-free path, target a ReadOnlySpan<T>.

When should you use them?

Almost always, when you are creating a collection from known values or merging collections. They are easier to read and the compiler keeps them efficient. The one time you cannot use them is when the target type is unknown, such as with var. In that case, give the variable an explicit type and you are good to go.

Collection expressions are not a niche trick. They are meant to become your normal, daily way of writing collections in modern C#. The more you use them, the cleaner your whole codebase becomes.

Quick recap

  • A collection expression uses square brackets [ ] to create a collection in a short, clear line. Example: int[] x = [1, 2, 3];.
  • The compiler picks the type from the target (the variable, return type, or parameter), so you do not repeat the type.
  • The spread element .. pours all items of one collection into another. Example: [..a, ..b] merges two collections.
  • One bracket style works for arrays, lists, Span<T>, ReadOnlySpan<T>, and more.
  • Targeting a span can store items on the fast stack with no heap allocation.
  • It started in C# 12 / .NET 8. C# 14 / .NET 10 added params spans and wider usage.
  • You cannot use them with var, because the type would be unknown. Give an explicit type instead.

References and further reading

Related Posts