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.
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.
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
Steps
See []
Compiler spots the bracket syntax
Read target type
Looks at the variable or return type
Pick builder
Array, List, Span, etc.
Create collection
Builds it the efficient way
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.
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.
| Task | Old style | Collection expression |
|---|---|---|
| Empty list | new List<int>() | [] |
| Array of numbers | new int[] { 1, 2, 3 } | [1, 2, 3] |
| Fill a list | new List<string> { "a", "b" } | ["a", "b"] |
| Merge two arrays | a.Concat(b).ToArray() | [..a, ..b] |
| Add item to front | manual 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.
| Place | Example |
|---|---|
| Variable | int[] x = [1, 2, 3]; |
| Return value | return [1, 2, 3]; |
| Method argument | Process([1, 2, 3]); |
| Default field value | private 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.
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); // 15How collection expressions grew over time
Steps
C# 12 / .NET 8
Brackets and spread element arrive
C# 13 / .NET 9
More targets and refinements
C# 14 / .NET 10
params spans, wider use, less allocation
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, WaterRead 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, becausevargives the compiler nothing to aim at. Write the type, likeint[] 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
paramsspans and wider usage. - You cannot use them with
var, because the type would be unknown. Give an explicit type instead.
References and further reading
- Collection expressions - C# reference (Microsoft Learn)
- Refactor your code with collection expressions (.NET Blog)
- What's new in C# 14 (Microsoft Learn)
- Improve Readability of Your Code with C# Collection Expressions (Anton Martyniuk)
- Breaking change: C# 14 overload resolution with span parameters (Microsoft Learn)
Related Posts
Getting Started with C# Records: A Beginner's Friendly Guide
Learn C# records the easy way: value equality, with expressions, positional syntax, and record struct, explained with simple real-life examples.
C# init-only and required Properties: A Beginner's Guide
Learn C# init-only and required properties with simple analogies, diagrams, and code. Build safe, immutable objects that are filled correctly every time.
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
Why I Write Tall LINQ Queries: Readable C# Pipelines
Learn why writing tall, one-operator-per-line LINQ queries in C# makes your code easier to read, debug, and review. Beginner friendly with diagrams.
Getting Started with Primary Constructors in .NET 8 and C# 12
Learn C# 12 primary constructors in .NET 8 the easy way: cleaner classes, fewer lines, dependency injection, and simple real-life examples for beginners.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.