Creating and initializing collections is something C# developers do almost every day. Whether you are working with arrays, lists, dictionaries, or other collection types, initializing them is a regular part of application development.
Traditionally, C# developers use different syntax depending on the collection type. Arrays use array initializers, lists use collection initializers, and other collection types may require factory methods or constructors.
Although these approaches work well, they can make everyday code unnecessarily verbose, especially when a method needs to combine multiple collections or initialize a collection with a few values.
Collection expressions provide a simpler way to create collections using square brackets. They allow developers to initialize different collection types with a consistent syntax and reduce unnecessary code.
Introduced in C# 12, collection expressions also support spread elements, which make it easier to combine existing collections without writing additional loops.
In this article, we will explore how collection expressions work, understand their practical applications, and see how they can make everyday C# code cleaner and easier to maintain.
Understanding Collection Expressions in C#
A collection expression is a simplified syntax for creating collection values. Instead of writing the collection type followed by an initializer, developers can use square brackets to define the elements.
Consider the traditional approach to creating an array:
int[] numbers = new int[] { 10, 20, 30, 40 };Using a collection expression, the same code becomes:
int[] numbers = [10, 20, 30, 40];The result is the same, but the second approach requires less syntax.
Collection expressions are not limited to arrays. They can also initialize lists, spans, and several collection interfaces.
For example:
int[] numbers = [10, 20, 30];
List<string> names = ["John", "David", "Sarah"];
Span<int> scores = [85, 90, 95, 100];
IEnumerable<string> technologies =
["C#", "ASP.NET Core", "SQL"];The important difference is that the target type determines which collection is created.
This feature makes collection initialization more consistent across different parts of an application.
Creating Arrays and Lists With Collection Expressions
Arrays and lists are among the most frequently used collection types in C#.
Before collection expressions, developers generally used different initialization syntax for these types.
Consider the following examples:
// Traditional array initialization
int[] numbers = new int[] { 1, 2, 3, 4 };
// Traditional list initialization
List<string> languages = new List<string>
{
"C#",
"Java",
"Python"
};With collection expressions, both can use the same syntax:
int[] numbers = [1, 2, 3, 4];
List<string> languages = ["C#", "Java", "Python"];This is particularly useful when initializing configuration values, application settings, supported file extensions, or predefined business rules.
For example, an application may support a fixed set of file extensions:
public static class FileSettings
{
public static readonly string[] AllowedExtensions =
[
".pdf",
".docx",
".xlsx",
".csv"
];
}The collection is easy to read and update without introducing unnecessary initialization code.
Using Empty Collection Expressions
One common requirement in application development is returning an empty collection when no records are available.
Traditionally, developers might write:
List<string> messages = new List<string>();
int[] numbers = Array.Empty<int>();Collection expressions provide a consistent alternative:
List<string> messages = [];
int[] numbers = [];
IEnumerable<int> values = [];The compiler determines the collection type from the target.
For an empty array, the compiler can use an optimized empty array instance where appropriate. However, the actual implementation depends on the target type and how the resulting collection is used.
There is one important restriction: collection expressions require a target type.
The following code does not compile:
var numbers = [10, 20, 30];The compiler cannot determine the intended collection type from the collection expression alone.
Instead, provide an explicit type:
int[] numbers = [10, 20, 30];Or use an explicitly typed variable declaration:
List<int> numbers = [10, 20, 30];Understanding target typing is important when using collection expressions in everyday development.
Combining Collections Using Spread Elements
One of the most useful features of collection expressions is the spread element.
The spread operator uses two consecutive dots (..) to include elements from an existing collection.
Consider an application that maintains separate collections for active and inactive users.
Using the traditional approach:
List<string> activeUsers =
new() { "John", "Sarah" };
List<string> inactiveUsers =
new() { "David", "Michael" };
List<string> allUsers = new(activeUsers);
allUsers.AddRange(inactiveUsers);With collection expressions, the same operation becomes:
List<string> activeUsers =
["John", "Sarah"];
List<string> inactiveUsers =
["David", "Michael"];
List<string> allUsers =
[.. activeUsers, .. inactiveUsers];The spread elements include all items from the existing collections.
You can also combine individual values with existing collections:
int[] firstBatch = [10, 20, 30];
int[] secondBatch = [40, 50, 60];
int[] allNumbers =
[0, .. firstBatch, .. secondBatch, 70];The resulting collection contains:
0, 10, 20, 30, 40, 50, 60, 70This approach is useful when combining application settings, merging predefined values, or creating a new collection from multiple sources.
Keep in mind that spreading a collection enumerates its elements to build the resulting collection. It does not create a lazy sequence like an unevaluated LINQ query.
Using Collection Expressions With Method Parameters
Collection expressions are also useful when passing collections directly to methods.
Consider a method that calculates the total of a set of numbers:
public static int CalculateTotal(IEnumerable<int> numbers)
{
return numbers.Sum();
}You can call this method without creating a separate array or list:
int total = CalculateTotal([10, 20, 30, 40]);The compiler uses the method parameter type to determine the target collection type.
This makes method calls shorter and avoids introducing temporary variables when the collection is only needed once.
Another example is passing supported file extensions to a validation method:
Join the conversation! Your thoughts help the community grow.