DEV Community

Cover image for Modern C# Development: Get Started With Params Collections
Lou Creemers
Lou Creemers

Posted on

Modern C# Development: Get Started With Params Collections

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

  1. 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>.

  1. Performance

Using params ReadOnlySpan<T> or params Span<T> can avoid heap allocations for many calls. Less GC pressure, tighter loops.

  1. 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.

  1. Works with C# 12 collection expressions

Collection expressions like [1, 2, 3] and spreads like [..existing] compose nicely with params.

  1. 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
Enter fullscreen mode Exit fullscreen mode

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}"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]);
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

Pattern friendly switches

static string Describe(params ReadOnlySpan<string> words) => words.Length switch
{
    0 => "empty",
    1 => $"one: {words[0]}",
    _ => $"many ({words.Length})"
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Gotchas and notes

  • params must be the last parameter, and you can have only one params parameter.
  • You cannot combine params with ref, out, or in.
  • Prefer ReadOnlySpan<T> for read only hot paths. Prefer Span<T> when you need in place edits. Prefer IEnumerable<T> or IReadOnlyList<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)