DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

`params` Collections in C# 13 — Beyond Arrays, Toward Flexibility

ParamsCollectionsInCSharp13

params Collections in C# 13 — Beyond Arrays, Toward Flexibility

The params keyword in C# has long enabled developers to create methods that accept a variable number of arguments—but until C# 13, this flexibility was limited to arrays only.

That limitation is now gone.

C# 13 expands support for params to include other collection types, such as:

  • Span<T>
  • ReadOnlySpan<T>
  • IEnumerable<T>
  • IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>
  • Any custom collection type with an Add method

In this post, you’ll explore:

  • How params works beyond arrays
  • Real examples with ReadOnlySpan<T> and List<T>
  • How the compiler handles interfaces
  • Best practices for adopting this feature

What’s New in C# 13?

Prior to C# 13, you could only use params with an array type:

public void Print(params string[] messages)
{
    foreach (var msg in messages)
        Console.WriteLine(msg);
}
Enter fullscreen mode Exit fullscreen mode

Now, you can use other collection types like this:

public void Concat<T>(params ReadOnlySpan<T> items)
{
    foreach (var item in items)
    {
        Console.Write(item);
        Console.Write(" ");
    }
    Console.WriteLine();
}
Enter fullscreen mode Exit fullscreen mode

✅ This eliminates unnecessary array allocations and unlocks stack-only data structures like Span<T>.


Examples

Using ReadOnlySpan<T> with params

public void Log(params ReadOnlySpan<char> lines)
{
    foreach (var line in lines)
        Console.WriteLine(line.ToString());
}

ReadOnlySpan<char> span1 = "Hello".AsSpan();
ReadOnlySpan<char> span2 = "World".AsSpan();

Log(span1, span2);
Enter fullscreen mode Exit fullscreen mode

Using List<T> with params

public void Merge(params List<string> entries)
{
    foreach (var list in entries)
        foreach (var item in list)
            Console.WriteLine(item);
}
Enter fullscreen mode Exit fullscreen mode

Using IEnumerable<T> interface

public void Report(params IEnumerable<int> values)
{
    foreach (var v in values)
        Console.WriteLine(v);
}
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, the compiler synthesizes a temporary list to hold the arguments, as long as the target interface or class:

  • Implements IEnumerable<T>
  • Has a public Add(T item) method (if it's a concrete collection)

Compiler Behavior

Target Type Compiler Behavior
Array (T[]) Packs args directly into array
Span<T> / ReadOnlySpan<T> Uses stack allocation if possible
IEnumerable<T> Synthesizes a list or compatible collection
Custom collection Uses Add method if available

This unlocks performance optimizations and better API ergonomics.


Constraints

  • You must still use a single params parameter as the last argument.
  • The type must either:
    • Be an array
    • Implement IEnumerable<T> and have a usable Add method
  • Overuse can lead to ambiguous overload resolution when mixing with arrays

✅ Best Practices

Tip Why it matters
Use Span<T> when stack-allocating params Avoids heap allocations
Use IEnumerable<T> when consuming any collection Supports maximal flexibility
Avoid params with large or deeply nested types Helps performance and readability
Be mindful with overloads and type inference Avoid ambiguity and type mismatches

Learn More


Final Thoughts

C# 13’s params collections represent a quiet but powerful evolution in API design. By allowing non-array collection types, developers can build safer, more efficient, and more ergonomic code — especially in performance-critical scenarios.

From spans to interfaces, the new flexibility means less boilerplate, fewer allocations, and better APIs.

Don’t just use paramsmaster it.


Written by: [Cristian Sifuentes] – .NET Core Specialist | C# API Designer | Clean Code Advocate

Share your experience with spans or params collections in the comments below.

Top comments (0)