Collection Expression Arguments Fix the Comparer Problem in C# 15
/ 3 min read
Found this post helpful?
Buy me a coffeeTable of Contents
Collection expressions gave us this wonderful syntax, [a, b, c], back in C# 12, and I reach for the feature
constantly. But the moment I
needed a case-insensitive HashSet, I had to drop the syntax and go back to a constructor call. C# 15 fixes that.
In this post, we’ll look at collection expression arguments: the new with(...) element that passes arguments to the collection being built. The feature is documented in What’s new in C# 15.
One note before we start: C# 15 ships with .NET 11, and you need <LangVersion>preview</LangVersion> in your project file to compile this today.
The Comparer Problem
A HashSet<string> defaults to ordinal, case-sensitive comparison which can be a problem with logically identical
values. Take the following example:
HashSet<string> tags = ["dotnet", "DotNet", "DOTNET"];// Count == 3. Three "different" strings.Most of the time I want “dotnet” treated as one tag. The fix is a comparer, and the only way to supply one was the constructor:
var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase){ "dotnet", "DotNet", "DOTNET",};// Count == 1.That works, but I just gave up the simplicity and clarity of a collection expression to get it. The initializer syntax is noisier, and I can’t use it anywhere an expression is expected, like a default parameter or an inline argument.
Passing Arguments with with(...)
C# 15 lets you keep the expression. Put a with(...) element first, and its arguments flow to the collection’s constructor:
HashSet<string> tags = [with(StringComparer.OrdinalIgnoreCase), "dotnet", "DotNet", "DOTNET"];// Count == 1.Same comparer, same result, but it’s still a collection expression. The with(...) element has to come first, and the
arguments match an existing constructor overload, so the compiler checks them like any other call.
One limit to know in the RC: this applies to collections like HashSet and List, not dictionaries. Dictionary expressions are a separate proposal that hasn’t shipped yet, so there’s no with(...) form for Dictionary to configure a comparer today. If you need a case-insensitive dictionary right now, the constructor is still your tool.
It’s Not Just Comparers
Any constructor argument is fair game. The obvious second use is capacity. If I know a list is about to hold a few thousand items, pre-sizing it avoids a string of reallocations as it grows:
int[] seed = [1, 2, 3];List<int> buffer = [with(capacity: 2048), .. seed];The feature also applies to named arguments and spreads, so it works with the collection expressions you’re writing now instead of replacing them.
For types that build through a factory or builder rather than a plain constructor, with(...) forwards the arguments to that path too, so custom collection types can opt in and get the same call-site configuration.
The Verdict
Collection expression arguments replace the one case that forced me out of collection expressions: needing to configure the collection itself. Before C# 15, a comparer or a capacity meant a constructor call and the longer initializer form. Now the configuration lives inside the brackets with the data.
Reach for with(...) whenever the collection, not just its contents, needs setup. In your codebase that’s typically
two situations: a comparer-sensitive HashSet, and a capacity hint on a collection. For everything else, plain [a, b, c] still works. Personally, it’s nice to see C# become less verbose and more expressive.