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:
- Each call allocates a new array on the heap (memory overhead)
- Frequent calls increase garbage collection pressure
- 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:
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!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.The collection passing story - When passing an existing collection:
Span<T>
still needs a.ToArray()
call which costs performanceIEnumerable<T>
shines here with lower allocations than array params- Direct array params still require a new array copy
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.