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.

Category
.NETC#

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.

10 comments

  • Pascal Arnold · Edited

    "If you pass a List, the params IEnumerable overload will be used"

    Shouldn't it work with the span too ? It would make it so much easier to update existing code to use spans when possible without having to either :
    1) Make 2 overload, with the "List" overload calling the Span overload
    2) Go to every caller and manually convert to span, which goes against the whole point of this new feature

    The entire use of...

    Read more
    • Kathleen DollardMicrosoft employee Author

      To clarify your quote, I'll include that paragraph:

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

      Read more
      • Kathleen DollardMicrosoft employee Author

        While what I said is true, there is actually a bigger issue between these specific types. An array is implicitly convertible to a span, but a List is not. Thus, if you removed the IEnumerable overload in the example, you would get a compiler error CS1503: Argument 1: cannot convert from ‘System.Collections.Generic.List’ to ‘System.Span’

  • Gabor Zavarko

    Hi. I’m curious about how safe it is to use the field keyword in our code. Can I count on it being included in the upcoming stable versions of C#, even as an optional feature that can be enabled? Perhaps with only minor modifications? I really like this feature, but I don’t want to do unnecessary work if we’ll have to revert it all later.

    • Kathleen DollardMicrosoft employee Author

      I feel confident that the design of using this feature is the one we want. The preview status is to allow time for feedback on the breaking change mechanism, which will not affect you unless you were previously using . I do not think this feature will change, but, all preview features are subject to change and might not be released.

      If you decide to use it, we'd really love to hear any feedback.

      Read more
  • Oliver Münchow (AixConcept GmbH)

    While from a technical perspective this is true, it just reads a little awkward, because a collection is formal less then a list.
    Many collection interfaces have a single implementation that would be a logical choice, such as ReadonlyCollection for IReadonlyList.

    Maybe another example feels a little more natural.

    • Kathleen DollardMicrosoft employee Author

      The ReadonlyCollection/IReadonlyList naming is an oddity in the .NET libraries API. That example clarifies that we are going for intent. The receiver said it could handle something readonly, so let’s give it something that is readonly. If I think of another example that clarifies that point well, I’ll change it due to the API naming.

  • Oliver Münchow (AixConcept GmbH) · Edited

    Minor typo in the text: … implicit conversion from array to ReadONlySpan.