November 14th, 2024

Calling methods is easier and faster with C# 13 params collections

Kathleen Dollard
Principal Program Manager

C# 13 brings features that make it easier, safer, and faster to write code in the styles you know and love. You’ll find a full list of C# 13 features at What’s new in C# 13.

C# 13 fulfills a long standing feature request by allowing params to be any of the collections supported by collection expressions, rather than just arrays. This feature builds on collection expressions that were introduced in C# 12.

Laying the groundwork with collection expressions

Collection expressions were introduced in C# 12 and offer an alternative to the myriad of ways that you previously created different kinds of collections. In the context of the method, this allows you to simplify use of collections in many scenarios, including calling methods:

// Prior to C# 12
WriteByteArray(new[] { (byte)1, (byte)2, (byte)3 });
WriteByteSpan(stackalloc[] { (byte)1, (byte)2, (byte)3 });

// After C# 12
WriteByteArray([1, 2, 3]);
WriteByteSpan([1, 2, 3]);

static void WriteByteArray(byte[] bytes) { }
static void WriteByteSpan(Span<byte> bytes) { }

This also allowed us to unify aspects of how C# works with collections. Note that collection expressions infer both the type of the collection and the type of the members.

params arrays

params has been in the language since C# 1.0. It allows calling code to include zero to many arguments as a comma delimited list. The method receives this list as an array, which might be empty:

WriteByteArray(1, 2, 3);
WriteByteArray();

static void WriteByteArray(params byte[] bytes) { }

Prior to C# 13, the params must be declared as an array in the method’s parameter list.

In addition to calling with a list of values, you have always been able to call methods that take a params with an array, and starting in C# 12, this can be a collection expression:

WriteByteArray(1, 2, 3);
WriteByteArray([1, 2, 3]);
byte[] bytes = [4, 5];
WriteByteArray([1, 2, 3, .. bytes]);

static void WriteByteArray(params byte[] bytes) { }

params collections

Starting in C# 13, params can be any of the collection types that support collection expressions:

WriteByteSpan(1, 2, 3);
WriteByteSpan();

static void WriteByteSpan(params Span<byte> bytes) { }

While that seems like a small change, it often enables the compiler to optimize your code. For example, if the method uses params Span<T>, the compiler can use stack space to create the span for the method. That performs better than allocating an array.

Using a specific type can also communicate to callers how the collection will be used. For example, params IReadonlyList<T> indicates that the collection will not be changed.

If you use params IEnumerable<T>, your users can pass a list of literal values, an array, a List<T>, any of the collection types that implement IEnumerable<T>, or a LINQ expression:

WriteByteArray(1, 2, 3);

byte[] bytes = [1, 2, 3, 4, 5];
WriteByteArray(bytes.Where(x => x < 4));

static void WriteByteArray(params IEnumerable<byte> bytes) { }

When the params receiver is an interface and the argument is a discrete list of elements or a collection expression, the compiler will use a concrete type. Many collection interfaces have a single implementation that would be a logical choice, such as ReadonlyCollection<T> for IReadonlyList<T>. Since IEnumerable<T> does not have such an obvious choice and is so fundamental to .NET, it uses a special high-performance type for both param and collection expressions.

Overloading

C# supports overloading methods – which means that multiple methods with the same name can exist, if they differ by the types of the parameters. You can overload on a params collection, as you would other parameters.

For example, you might have an overload with params IEnumerable<T> and one with params ReadOnlySpan<T> in the same resolution scope. If you pass a list of values, the params ReadOnlySpan<T> overload will be selected. If you pass an array, the params ReadOnlySpan<T> overload will also be selected, because there is an implicit conversion from array to ReadONlySpan<T>. If you pass a List<T>, the params IEnumerable<T> overload will be used:

public class ParamCollections
{
    public static void Overloads()
    {
        WriteNumbers(1, 2, 3);
        WriteNumbers([1, 2, 3]);
        WriteNumbers(new[] { 1, 2, 3 });
        byte[] ints = [1, 2, 3, 4, 5];
        WriteNumbers(ints.Where(x => x < 4));

    }

    // This code shifts from static local functions to private functions 
    // because overloading is not allowed for local functions.    
    private static void WriteNumbers<T>(params IEnumerable<T> values) => Console.WriteLine("IEnumerable");
    private static void WriteNumbers<T>(params ReadOnlySpan<T> values) => Console.WriteLine("Span");
}

// The result is:
// 
// Span
// Span
// Span
// IEnumerable

The compiler picks a reasonable overload for you. It will often be Span<T> if it is available because this saves an allocation in the method call.

params collections will make your applications faster, whether or not you add them to your own code, because the .NET runtime libraries can now use high performance types like Span<T> in more places. You use the same concepts you’ve always used for params, callers have more flexibility in how they call the method, and the compiler picks the overload.

Considering overloads

Overloads are a very powerful feature of C#. But as with many powerful things, proper usage is important. If there are multiple methods with the same name, they should do the same thing. They may differ in how they do it or differ by performance, but changing the types passed should not introduce breaking changes in your application. Often, this means calling a single implementation after adjusting the argument values.

As you can see in the example above, small changes in the calling code can result in different overloads being called. This is true for all C# applications, whether or not you use params collections.

Summary

We’re excited about params collection in C# 13 and can’t wait to hear what you think!

You can learn about all the features in C# 13 at What’s new in C# 13.

Author

Kathleen Dollard
Principal Program Manager

Kathleen Dollard loves to code and loves to teach and talk about code. She’s written tons of articles, a book, and spoken around the world. She’s on the .NET Team at Microsoft where she works on the .NET Core CLI and SDK and managed languages (C# and Visual Basic.NET). She’s always ready to help developers take the next step in exploring the wonderful world we call code.

0 comments