The upcoming release of .NET 9 brings with it a lot of exciting new features. One in particular stands out to me: enhanced support for params collections. If you’ve ever worked with C#’s params keyword, you’ll know it’s been a useful tool for creating methods that accept a number of arguments. But, until now, the params keyword was limited to arrays and had been lacking in performance. These limitation meant developers had to use a single-dimensional arrays, regardless of whether the underlying type was better suited for something else, such as a list or span OR avoid the use of this keyword altogether.

What Is params?

For anyone new to C#, the params keyword lets you pass a variable number of arguments to a method. This is handy when you don’t know ahead of time how many arguments will be passed. The classic example is something like this:

void PrintNumbers(params int[] numbers)
{
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

With this method, you can pass any number of integers:

PrintNumbers(1, 2, 3, 4, 5);
PrintNumbers(1, 2, 3, 4, 5, 7, 8, 40);

Up until .NET 9, the params keyword only worked with single-dimensional arrays. This meant if you wanted to pass a List<T>, Span<T>, or any other collection type, you had to convert it to an array first. Before .NET 9 and C# 13, the params keyword required you to pass arguments as an array, whether you needed an array or not. This led to some hidden performance overhead, especially in performance-sensitive code or in scenarios where the method was invoked many times. So the above scenario caused allocations even though you did not need an array.

params Collections in .NET 9

Now, in .NET 9, the params keyword is no longer limited to arrays. You can use it with any collection type that implements IEnumerable<T> and has an Add method. This enhancement dramatically improves flexibility when passing a collection of arguments.

Let’s take a look at how things work post-C# 13 by writing a benchmark:

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class StringBuilderVsConcat
{
    [Benchmark]
    public string Concat()
    {
        return string.Concat("a", "b", "c", "d", "e");
    }

    [Benchmark]
    public string StringAdd()
    {
        var a = "a";
        var b = "b";
        var c = "c";
        var d = "d";
        var e = "e";
        return a + b + c + d + e;
    }
}

Looking at the results, we can see that the Concat method is now significantly better when it comes to memory allocations:

MethodJobRuntimeMeanErrorAllocated
Concat.NET 8.0.NET 8.024.62 ns0.533 ns96 B
StringAdd.NET 8.0.NET 8.023.00 ns0.445 ns96 B
Concat.NET 9.0.NET 9.028.01 ns0.575 ns32 B
StringAdd.NET 9.0.NET 9.032.49 ns0.666 ns96 B

32 bytes against 96 bytes. Awesome right?

Why This Matters

Type Flexibility

Previously, methods that needed to accept collections of varying sizes had to make do with arrays or rely on multiple method overloads. This was cumbersome, and when a method had to handle various collection types—such as List<T>, Span<T>, or ReadOnlySpan<T>—it meant duplication of effort. With the new changes, the params keyword can handle all of these with one method signature.

public void Concat<T>(params ReadOnlySpan<T> items)
{
    for (int i = 0; i < items.Length; i++)
    {
        Console.Write(items[i]);
        Console.Write(" ");
    }
    Console.WriteLine();
}

You can now call this method with a Span<T>, ReadOnlySpan<T>, or even just individual arguments without any extra conversion steps.

Improved Performance with Spans

One of the most notable improvements comes from the ability to use Span<T> and ReadOnlySpan<T>. These types are highly efficient for working with slices of data without needing to allocate new arrays. In performance-critical applications, this allows you to gain the benefits of the params keyword without sacrificing efficiency.

For example, a method that accepts a ReadOnlySpan<T> can now be used to pass spans directly, allowing developers to take advantage of the low allocation cost of spans when passing large data sets.

What Interfaces and Types Are Supported?

The list of types you can now use with the params keyword is extensive. In addition to arrays, the following are now supported:

  • IEnumerable<T>
  • ICollection<T>
  • IList<T>
  • IReadOnlyCollection<T>
  • IReadOnlyList<T>
  • Span<T>
  • ReadOnlySpan<T>

This means that if you’re using a custom collection or one of the many built-in collection types in .NET, you can now use it with params as long as it adheres to these interfaces and implements an Add method.

Wrapping Up

The changes to params in .NET 9 may seem subtle at first, but they offer a huge leap forward in flexibility and performance for developers. By allowing more collection types—especially spans—C# 13 expands the scenarios where params can be applied, reduces boilerplate code, and allows for more efficient memory usage.

Affiliate promo

If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!

Leave a comment

Trending