Hi lovely readers,
We have all written APIs that want a flexible "give me as many values as you like" parameter, then ended up juggling arrays and awkward call sites. Modern C# fixes this in two steps. C# 12 added collection expressions ([1, 2, 3]
). C# 13 lets params
work with more than arrays.
In this blog post, we are going to focus on params collections and how to use them cleanly.
Does your application pass lots of values to helpers? Do you want fewer allocations and cleaner call sites? Do you want to pair collection expressions with a smarter params
? Continue reading and I will show you how.
Why params collections
- Simplicity and readability
Call methods with a natural, comma separated list. You can write Sum(1, 2, 3)
while the API accepts params ReadOnlySpan<int>
.
- Performance
Using params ReadOnlySpan<T>
or params Span<T>
can avoid heap allocations for many calls. Less GC pressure, tighter loops.
- The right type for the job
Choose IEnumerable<T>
when you only need to iterate, IReadOnlyList<T>
when you want indexing, or spans when you want minimal overhead.
- Works with C# 12 collection expressions
Collection expressions like [1, 2, 3]
and spreads like [..existing]
compose nicely with params
.
- Fewer overloads to maintain
One params
signature often replaces several "convenience" overloads.
How to use params collections
Initialize a params
span (fast path)
using System;
static int Sum(params ReadOnlySpan<int> values)
{
var total = 0;
foreach (var n in values) total += n;
return total;
}
// Clean calls:
Console.WriteLine(Sum(1, 2, 3)); // 6
Console.WriteLine(Sum()); // 0
Console.WriteLine(Sum([4, 5, 6])); // 15
Why this is nice: literals and many temporaries become allocation free, arrays convert to spans implicitly, and the method communicates read only intent.
Tip: add
scoped
for extra safety with spans
static void Print(params scoped ReadOnlySpan<char> chars) { /* ... */ }
Use an interface when flexibility matters
using System;
using System.Collections.Generic;
using System.Linq;
static void Log(params IEnumerable<string> parts)
{
Console.WriteLine(string.Join(" | ", parts));
}
Log("alpha", "beta"); // comma separated
Log(["gamma", "delta"]); // collection expression
Log(new[] { "epsilon", "zeta" }); // array
Log(new List<string> { "eta", "theta" }); // list
Log(Enumerable.Range(1,3).Select(i => $"v{i}"));
When params
targets an interface, callers can pass almost anything iterable.
C# 12 vs C# 13 calling styles
-
C# 12: you could pass a single collection expression to compatible parameters, for example
WriteSpan([1, 2, 3])
. -
C# 13: the expanded
a, b, c
form works with non array params such as spans and common interfaces.
// C# 12 style
static void WriteSpan(Span<byte> data) { /* ... */ }
WriteSpan([1, 2, 3]);
// C# 13 params collections
static void WriteSpan(params Span<byte> data) { /* ... */ }
WriteSpan(1, 2, 3); // expanded form
WriteSpan(); // empty is fine
Calling styles and formatting
You can mix and match expanded arguments, arrays, lists, and collection expressions. This keeps call sites tidy and expressive.
static void Render(params IReadOnlyList<string> items)
{
foreach (var s in items) Console.WriteLine($"• {s}");
}
var list = new List<string> { "apples", "bananas" };
// All of these work
Render("cherries", "dates");
Render(["eggs", "figs"]);
Render([..list, "grapes"]);
Logic with params collections
Checking argument count
static void EnsureAtLeastOne(params ReadOnlySpan<int> nums)
{
if (nums.Length == 0)
throw new ArgumentException("Give me at least one number.");
}
Pattern friendly switches
static string Describe(params ReadOnlySpan<string> words) => words.Length switch
{
0 => "empty",
1 => $"one: {words[0]}",
_ => $"many ({words.Length})"
};
Mutating with Span<T>
static void Increment(params Span<int> vals)
{
for (int i = 0; i < vals.Length; i++) vals[i]++;
}
var data = new[] { 1, 2, 3 };
Increment(data); // array converts to Span<int>
Console.WriteLine(string.Join(",", data)); // 2,3,4
Interop with JSON and serialization
params
is a call site feature. When data comes from JSON, you typically deserialize to a collection, then pass it to your params
method in one of these ways:
using System.Text.Json;
// Imagine incoming JSON: [1,2,3]
var arr = JsonSerializer.Deserialize<int[]>("[1,2,3]");
// Any of these calls are fine
Console.WriteLine(Sum(arr!)); // array
Console.WriteLine(Sum([..arr!])); // collection expression
You can keep your APIs clean and still play nicely with serialized payloads.
Conditions
You can use if
and switch
with params
data just like any other collection.
static void Validate(params ReadOnlySpan<TimeSpan> delays)
{
if (delays.Length > 10)
throw new InvalidOperationException("Too many delays.");
}
Compare sequences
Here is a small helper that counts how many times adjacent values differ. It uses params ReadOnlySpan<int>
so common call sites are allocation free.
static int CountTransitions(params ReadOnlySpan<int> values)
{
int count = 0;
for (int i = 1; i < values.Length; i++)
if (values[i] != values[i - 1]) count++;
return count;
}
Console.WriteLine(CountTransitions(1, 1, 2, 2, 3)); // 2
Gotchas and notes
-
params
must be the last parameter, and you can have only oneparams
parameter. - You cannot combine
params
withref
,out
, orin
. - Prefer
ReadOnlySpan<T>
for read only hot paths. PreferSpan<T>
when you need in place edits. PreferIEnumerable<T>
orIReadOnlyList<T>
when you want flexibility or indexing.
That is a wrap
Params collections make APIs nicer to call and often faster, especially when you pick spans for performance sensitive paths. Pair them with collection expressions from C# 12 and you get clean call sites, fewer allocations, and clearer intent. Whether you are building utility libraries, formatting helpers, or high throughput services, params collections can simplify your codebase.
I hope this post helped you understand params collections in modern C#. If you have any questions or comments, feel free to reach out on @lovelacecoding on almost every social media platform or leave a comment down below.
See ya!
Top comments (0)