Browse C# 15

C# 15: Collection Expression Arguments

C# 15 extends collection expressions with the with keyword, letting you pass constructor arguments like capacity or a comparer directly inside the expression.

There's also a video on this topic using different examples, and the two complement each other.

C# 12 introduced collection expressions - a clean and succinct syntax for initializing collections. C# 15 extends them with collection expression arguments, letting you pass constructor arguments like an initial capacity or a comparer directly inside the expression using the with keyword.

The problem

Consider a string[] of seed destinations that you need to merge into a larger, mutable list:

string[] seedCities = ["London", "Paris", "Berlin", "Tokyo"];
List<string> route = [..seedCities];
route.Add("Sydney");

This works, but List<T> starts with a default internal capacity of 4. Adding a fifth element forces the list to double its internal array - all existing items are copied into a new, larger array. If this occurs often enough, these repeated allocations add up and degrade performance.

The existing fix

Provide the capacity upfront via the constructor, then populate separately:

string[] seedCities = ["London", "Paris", "Berlin", "Tokyo"];
List<string> route = new(100);
route.AddRange([..seedCities, "Sydney"]);

This works well. The initial capacity is large enough that no resize will occur until the list grows past 100 items. The catch is that there are two separate steps - first step is to set the capacity during instantiation and the second step is to add new items.

The C# 15 fix

With collection expression arguments, the capacity moves inside the expression:

string[] seedCities = ["London", "Paris", "Berlin", "Tokyo"];
List<string> route = [with(100), ..seedCities, "Sydney"];

The with(...) argument is passed to the collection’s creation method, typically its constructor, before the elements are added. The result is identical to the two-step version above, but the intent is expressed in one place.

Passing a comparer

The same syntax works for any constructor argument. Take a HashSet<string> that should treat continent names as case-insensitive:

// Without a comparer - case-sensitive, all four are distinct
HashSet<string> continents = ["Europe", "europe", "EUROPE", "EuRoPe"];
// Prints: Europe, europe, EUROPE, EuRoPe

// With a comparer - case-insensitive, treated as one
HashSet<string> continents = [with(StringComparer.OrdinalIgnoreCase), "Europe", "europe", "EUROPE", "EuRoPe"];
// Prints: Europe

The comparer is passed through with(...) to the HashSet<string> constructor. From that point on, every element added, during instantiation or after, is deduplicated using it.

The takeaway

Collection expression arguments let you keep clean initialization syntax without losing control over how the collection is built. If you’ve been using new List<T>(capacity) before adding items, you can now do it all in one expression.

Related reading