Ever used the params keyword in C#? If you write C# code regularly, you probably reach for it whenever you need to pass a variable number of arguments to a method. It’s super handy, letting you skip the tedious step of creating arrays first.

But until now, there’s been a limitation: params only worked with arrays. This meant every call created memory allocations with potential performance costs. The good news? C# 14 is changing the game by extending params to work with modern collections like IEnumerable<T>, Span<T>, and more.

How params Has Worked Until Now

Let’s look at how we’ve all been using params up to this point:

// Traditional params with arrays
public void DisplayNames(params string[] names)
{
    foreach (var name in names)
    {
        Console.WriteLine(name);
    }
}

// Usage
DisplayNames("Alice", "Bob", "Charlie");
// Behind the scenes: compiler creates a new string[] array

This pattern works, but brings some notable drawbacks:

  1. Each call allocates a new array on the heap (memory overhead)
  2. Frequent calls increase garbage collection pressure
  3. Arrays offer less flexibility than other collection types

The New params: Supporting Collections

C# 14 broadens the params keyword to work with various collection types:

  • IEnumerable<T>
  • ReadOnlySpan<T>
  • Span<T>
  • Other collection interfaces

Here’s what this looks like in real code:

// With IEnumerable<T>
public void ShowItems(params IEnumerable<string> items)
{
    foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

// With Span<T>
public void ProcessValues(params Span<int> values)
{
    for (int i = 0; i < values.Length; i++)
    {
        values[i] *= 2; // Can modify values with Span<T>
    }
}

Performance Implications

Let’s talk about the real-world performance benefits this change brings:

Reduced Allocations

With Span<T> or ReadOnlySpan<T>, you can skip heap allocations completely since these types can work with stack-allocated memory:

// No heap allocations needed
CalculateSum(params Span<int> values)
{
    int sum = 0;
    foreach (var value in values)
    {
        sum += value;
    }
    return sum;
}

var result = CalculateSum(1, 2, 3, 4, 5); // Uses the stack, not the heap

Better Fit with Modern APIs

Many newer .NET APIs use Span<T> for better performance. Now your code can fit right in with these patterns:

// Much cleaner than creating arrays just to pass them
public bool TryParseValues(params Span<char> characters)
{
    // Work directly with the span without extra memory use
    return ProcessSpanDirectly(characters);
}

Memory Usage: Before and After

Compare these two approaches:

// Old way
public void ProcessTraditional(params int[] values)
{
    // Process array values
}

// New way
public void ProcessModern(params ReadOnlySpan<int> values)
{
    // Process span values
}

// Old: Creates a new array on the heap
ProcessTraditional(1, 2, 3, 4, 5);

// New: No heap allocation needed
ProcessModern(1, 2, 3, 4, 5);

For high-performance code or methods called frequently, these savings add up quickly.

Practical Use Cases

Better API Design

This change makes your APIs more flexible and intuitive:

// Now we can work directly with collections
public async Task ProcessBatchAsync(params IEnumerable<DataItem> batches)
{
    foreach (var batch in batches)
    {
        await ProcessItemsAsync(batch);
    }
}

Efficient Data Processing

For binary data or networking code, Spans are a perfect fit:

public void ParsePacket(params Span<byte> packetData)
{
    // Work with chunks of data without copying
    var header = packetData.Slice(0, 4);
    var body = packetData.Slice(4);

    ProcessHeader(header);
    ProcessBody(body);
}

Cleaner LINQ Code

LINQ operations become more straightforward:

// Old way with array
public IEnumerable<T> CombineSources<T>(params IEnumerable<T>[] sources)
{
    return sources.SelectMany(source => source);
}

// New way, more direct
public IEnumerable<T> CombineSources<T>(params IEnumerable<T> sources)
{
    return sources.SelectMany(source => source);
}

Real-world Benchmarks

Talk is cheap, let’s look at some actual numbers. I ran benchmarks to compare the different params approaches using BenchmarkDotNet. Here’s the benchmark code:

[MemoryDiagnoser]
public class ParamsBenchmarks
{
    // Traditional array params method
    [Benchmark(Baseline = true)]
    public int SumWithArrayParams() => SumArray(1, 2, 3, 4, 5);

    public int SumArray(params int[] numbers)
    {
        int sum = 0;
        foreach (var num in numbers)
            sum += num;
        return sum;
    }

    // New IEnumerable params method
    [Benchmark]
    public int SumWithIEnumerableParams() => SumEnumerable(1, 2, 3, 4, 5);

    public int SumEnumerable(params IEnumerable<int> numbers)
    {
        int sum = 0;
        foreach (var num in numbers)
            sum += num;
        return sum;
    }

    // New Span params method
    [Benchmark]
    public int SumWithSpanParams() => SumSpan(1, 2, 3, 4, 5);

    public int SumSpan(params Span<int> numbers)
    {
        int sum = 0;
        foreach (var num in numbers)
            sum += num;
        return sum;
    }

    // Benchmark with existing collection
    private readonly List<int> _existingCollection = new() { 1, 2, 3, 4, 5 };

    [Benchmark]
    public int SumArrayWithExistingCollection() => SumArray(_existingCollection.ToArray());

    [Benchmark]
    public int SumEnumerableWithExistingCollection() => SumEnumerable(_existingCollection);

    [Benchmark]
    public int SumSpanWithExistingCollection() => SumSpan(_existingCollection.ToArray());
}

And here are the results:

| Method                              |      Mean |    Error |   StdDev | Ratio |   Gen0 | Allocated | Alloc Ratio |
| ----------------------------------- | --------: | -------: | -------: | ----: | -----: | --------: | ----------: |
| SumWithArrayParams                  |  6.747 ns | 0.329 ns | 0.945 ns |  1.00 | 0.0076 |      48 B |        1.00 |
| SumWithIEnumerableParams            | 24.870 ns | 0.806 ns | 2.286 ns |  3.75 | 0.0166 |     104 B |        2.17 |
| SumWithSpanParams                   |  5.610 ns | 0.147 ns | 0.304 ns |  0.85 |      - |         - |        0.00 |
| SumArrayWithExistingCollection      | 14.677 ns | 0.546 ns | 1.531 ns |  2.22 | 0.0076 |      48 B |        1.00 |
| SumEnumerableWithExistingCollection | 22.623 ns | 2.683 ns | 7.826 ns |  3.41 | 0.0064 |      40 B |        0.83 |
| SumSpanWithExistingCollection       | 19.189 ns | 1.070 ns | 3.105 ns |  2.90 | 0.0076 |      48 B |        1.00 |

What These Numbers Tell Us

Looking at these results, a few things jump out:

  1. Span is lightning fast for direct arguments - Span<T> params are about 15% faster than array params when passing literal values, and they allocate zero memory. That’s not a typo, no allocations at all!

  2. IEnumerable is convenient but costly - While IEnumerable<T> params offer flexibility, they’re almost 4x slower than arrays and allocate twice as much memory when passing direct arguments.

  3. The collection passing story - When passing an existing collection:

    • Span<T> still needs a .ToArray() call which costs performance
    • IEnumerable<T> shines here with lower allocations than array params
    • Direct array params still require a new array copy
  4. The memory story - The Gen0 and Allocated columns show how much garbage collection pressure each approach creates:

    • Span<T> with direct arguments: zero allocations
    • Array params: 48 bytes
    • IEnumerable<T> params: 104 bytes (highest)

The takeaway? For hot paths where performance matters most, Span<T> params give you a clear win with both speed and zero allocations. For methods that accept existing collections, IEnumerable<T> params can save you from unnecessary copying.

Conclusion

C# 14’s extension of the params keyword to work with collections is a game-changer for many codebases. It brings real benefits: more flexible APIs, less memory usage, and better performance where it counts. By supporting modern collection types beyond just arrays, C# keeps up with how developers actually write code today, especially in performance-sensitive scenarios.

When you upgrade to C# 14, take a look at your existing params methods. You might find places where switching to Span<T> or IEnumerable<T> could make your code faster and cleaner, particularly in hot paths or frequently called methods.