Updated on

The params keyword lets a C# method take a variable number of arguments: none, one, or a comma-separated list. A method can have only one params parameter, and it must be the last one.

Since C# 13, that parameter doesn’t have to be an array. We’ll go through the rules, the use cases and the best practices.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

How Does params Work in a Method Call?

params is a special keyword that allows passing a variable number of parameters into a method. It allows a nice, flexible syntax that helps us specify:

  • One or multiple parameters separated by commas (that’s the ‘killer feature’)
  • No parameters at all
  • A single parameter of a single-dimensional array type

Let’s see how it looks.

First, we define a method that accepts the params argument:

public class ShoppingList
{
    public void Add(params string[] items)
    {
        foreach (var item in items)
        {
            Console.WriteLine($"Added: {item}");
        }
    }
}

Here, params is followed by a single-dimensional array. Until C# 13 that was the only type it accepted.

Within the method body, it doesn’t matter that the values were passed as a params argument. This is completely transparent, and the variable behaves exactly like any other array variable.

Let’s have a look at the various valid ways of calling this method. First off, the ‘free flow’ of arguments:

var shoppingList = new ShoppingList();
shoppingList.Add("Bananas", "Grapes", "Ham", "Cheese");
shoppingList.Add("Headphones");

As you can see, we can add one or many arguments directly within the method, without declaring any collections to wrap the parameters. 

Secondly, we can also call the method with a single parameter – a single-dimensional array of the type specified in the signature:

string[] groceries = ["Bananas", "Grapes", "Ham", "Cheese"];
shoppingList.Add(groceries);

Lastly, it’s perfectly acceptable to not pass any value and omit the argument completely:

shoppingList.Add();

Notice that in this case, the value of the argument is an empty array. Omitting the argument does not pass a null value. However, it is possible to pass a null params argument to the method – and in that case, the argument will be null in the method body too. In our Add() method, the foreach loop then throws a NullReferenceException.

How Many Params Keywords Are Permitted in a Method?

Only one. A method can declare a single params parameter, and it has to be the last parameter in the list. Microsoft Learn says: “You can only use one params keyword in a method declaration.”

If the params parameter must come last, there is room for only one. That’s why declaring two of them fails with the same compiler error as putting one in the wrong place, CS0231.

A few more rules apply to the parameter itself. It can’t have a default value, but we don’t miss one, because leaving the argument out already gives us an empty collection. It can’t be combined with ref, out or in. And it must be a collection type: a single-dimensional array, or since C# 13 a span, a list or a collection interface.

Before it, anything goes. We can declare as many ordinary parameters as we like, even of the same type.

Default values belong to optional parameters, which follow different rules.

We can, however, combine params arguments with other parameters in the method signature, even of the same type. Consequently, the following method signatures are perfectly valid:

public void StringBeforeStringParams(string requiredStringArgument, params string[] items)

In this case, the compiler will expect at least one string passed to the method. It will treat all the following strings as values for the params argument.

We can also declare multiple arrays of the same type:

void ArrayBeforeParams(string[] requiredArray, params string[] items)

The method above can be called in two ways:

ArrayBeforeParams(["Required", "Array"], "And", "Free", "Arguments");
ArrayBeforeParams(["Required", "Array"], ["And", "Array", "For Params"]);

We can also specify optional arguments in the method signature:

void OtherArgumentsBeforeParams(int requiredParam, bool optionalBool = true, params string[] items)

Notice that here, even though we have specified the boolean parameter as optional, we cannot omit it and still pass the params values one by one:

OtherArgumentsBeforeParams(2, "Ooops", "Doesn't", "compile!");

The code above will show the “Argument 2: cannot convert from ‘string’ to ‘bool’” error until we explicitly specify a value for the optionalBool argument:

OtherArgumentsBeforeParams(2, true, "Yeah", "Now", "All good");

Another thing worth mentioning is that params doesn’t work well with named arguments:

void UseParamsWithNamedArguments(int firstArgument, params string[] items){ ... } 

UseParamsWithNamedArguments(firstArgument: 10, items: "Ooops", "Doesn't", "Compile");

The sample above shows a “No overload for method ‘UseParamsWithNamedArguments’ takes 4 arguments” compiler error. In order to use named arguments, we need to wrap the comma-separated values into an array:

UseParamsWithNamedArguments(firstArgument: 10, items: ["Now", "all", "good"]);

A single value is the exception. UseParamsWithNamedArguments(firstArgument: 10, items: "Single") compiles, and items holds one element.

Since C# 12, lambda expressions can declare a params parameter too, as we show in our article on default values and params in lambda expressions.

Can a params Parameter Be Something Other Than an Array?

Yes, since C# 13. Before that version, the params parameter had to be a single-dimensional array, which is what our first examples use.

Now we can also declare it as Span<T>, ReadOnlySpan<T>, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T> or IList<T>. Concrete types like List<T> work too, as long as they implement IEnumerable<T>, have a parameterless constructor and have an Add() method that takes one value.

The calling code doesn’t change. We still pass a comma-separated list, a ready-made collection, or nothing at all. Inside the method, the parameter is an ordinary variable of its declared type, just like the array was.

With params ReadOnlySpan<T>, the compiler doesn’t need to allocate an array on every call, so it suits small helpers on hot paths. With params IEnumerable<T>, the method takes a LINQ query or a list as it is, with no ToArray() call.

Multi-dimensional arrays are still not allowed, and neither is Dictionary<TKey, TValue>, because its Add() takes two arguments.

Let’s add two methods to our ShoppingList class:

public void AddRange(params IEnumerable<string> items)
{
    foreach (var item in items)
    {
        Console.WriteLine($"Added: {item}");
    }
}

public int Count(params ReadOnlySpan<string> items) => items.Length;

We call them exactly the way we call Add(). The difference shows when we already hold a LINQ query, because AddRange() takes it as it is:

shoppingList.AddRange("Milk", "Eggs");

List<string> pantry = ["Rice", "Beans", "Pasta"];
shoppingList.AddRange(pantry.Where(item => item.StartsWith('B')));

var count = shoppingList.Count("Bananas", "Grapes", "Ham");

The first call prints “Added: Milk” and “Added: Eggs”, the second prints “Added: Beans”, and count is 3.

What if a class has both an array overload and a span overload? Let’s try it with a small Basket class:

public class Basket
{
    public void Store(params string[] items)
        => Console.WriteLine($"Array overload: {items.Length} items");

    public void Store(params ReadOnlySpan<string> items)
        => Console.WriteLine($"Span overload: {items.Length} items");
}

Now we call Store() once with a comma-separated list and once with an array:

var basket = new Basket();
basket.Store("Bananas", "Grapes");

string[] fruits = ["Bananas", "Grapes"];
basket.Store(fruits);

As you can see, the list goes to the span overload and the array goes to the array overload:

Span overload: 2 items
Array overload: 2 items

The .NET libraries do the same. string.Join(), string.Format(), Path.Combine() and Task.WhenAll() all have a params ReadOnlySpan<T> overload next to the old array one, so existing calls with a list of arguments pick the span version once we recompile against .NET 9 or later. For a refresher on how the compiler chooses between overloads, check out our article on method overloading in C#, and for spans themselves, how to use Span to improve application performance.

What we declareWhat the compiler says (.NET 10)
params parameter followed by another parameterCS0231: A params parameter must be the last parameter in a parameter list
Two params parameters in one methodCS0231, the same error
params string[] items = nullCS1751: Cannot specify a default value for a parameter collection
ref params string[] itemsCS8328: The parameter modifier 'params' cannot be used with 'ref'
params string[,] itemsCS0225: The params parameter must have a valid collection type
params Dictionary<string, int> itemsCS9215: Collection expression type 'Dictionary<string, int>' must have an instance or extension method 'Add' that can be called with a single argument.
params List<int>, params IEnumerable<int>, params ReadOnlySpan<int>Compiles (C# 13 and later)

Why Use the ‘Params’ Keyword?

As mentioned, using the params keyword adds a bit of flexibility and it can make our methods more user-friendly.

On the other hand, it has some downsides. It introduces some ambiguity in methods that take multiple parameters because it’s less explicit about what methods require.

A significant problem with this happens when a method signature changes (e.g. someone adds or removes an argument). The calling code might still compile, but assign incorrect values to arguments where we used params argument with a comma-separated list of values. This is especially dangerous in case of params object[] because this argument type will accept any value type.

Arrays make params object[] even trickier. Let’s pass two arrays to a method that only counts its arguments:

static int CountObjects(params object[] items) => items.Length;

string[] names = ["Bananas", "Grapes"];
int[] quantities = [3, 5];

CountObjects(names);      // 2
CountObjects(quantities); // 1

The string[] converts to object[], so its elements become the arguments. An int[] can’t convert to object[], so the whole array arrives as a single argument. Both calls look the same, and the compiler warns about neither.

For this reason alone, the benefits of this keyword are rarely worth the risk, especially in public-facing APIs.

Conclusion

In this article, we’ve covered the params keyword along with several ways of using it. A method can have only one params parameter, it has to come last, and since C# 13 it can be a span, a list or a collection interface instead of an array. We’ve also mentioned the risks that are associated with this feature.

As always, we are responsible for choosing the right tools for the job, and hopefully, this article helps make an informed decision about using this feature. For other similar guidelines, be sure to check out our C# basics series.

Tested with .NET 10.0.10 and NUnit 5.0.0.