C# Params Collection: Syntax, Behavior, and Pitfalls
Learn how C# params handles variable arguments with arrays and the C# 13 params collections feature, including syntax, performance tradeoffs, and common mistakes.
The params keyword in C# lets a method accept a variable number of arguments. When a parameter is declared as params int[] numbers, callers can pass either an existing int[] array or a comma-separated list of integers; in the second case, the compiler creates an array before the method body runs. The classic form is powerful, and C# 13 extends the same idea to several collection types under the name params collections.
The Classic params Array Behavior
public static int Sum(params int[] numbers) { return numbers.Sum(); } int result1 = Sum(1, 2, 3); int[] values = { 4, 5, 6 }; int result2 = Sum(values);
In the original array form, params can appear only on the last parameter, and the parameter type must be a single-dimensional array. This has been true since C# 1.0. The array form remains the most widely compatible choice.
How the Compiler Handles Implicit Array Creation
When callers pass individual arguments, the compiler generates code that creates a new array and copies those arguments into it. That allocation happens on every call. When callers pass an array directly, the compiler uses that array reference without copying.
int[] data = { 1, 2, 3 }; Sum(data); // Uses data directly; no array copy Sum(1, 2, 3); // Compiler creates int[3] and copies the values
Because the parameter and the caller's expression refer to the same array instance, modifying an element inside the method changes the caller's array. This behavior is often harmless when the method only reads the values, but it can be surprising if the method reassigns elements. If you need to promise that the method will not modify the input, document that contract or accept a ReadOnlySpan<T> type when the language version permits it.
Introducing params Collections in C# 13
Starting with C# 13, params can be used with collection types other than single-dimensional arrays, such as IEnumerable<T>, List<T>, Span<T>, and ReadOnlySpan<T>. This feature is named params collections. It reuses the collection-expression machinery added in C# 12, so the compiler creates the requested collection type from the caller's argument list.
public static int Sum(params IEnumerable<int> numbers) { return numbers.Sum(); } int result = Sum(1, 2, 3, 4);
When the parameter type is IEnumerable<T>, the compiler typically creates a temporary list or array and wraps it as the interface. The exact lowering depends on the target collection type and the compiler version. Because params collections require C# 13, an older codebase should keep using arrays or accept an explicit collection parameter.
Performance Implications: Array vs Collection
The classic array-based params has a predictable allocation pattern: passing individual arguments always allocates a new array, while passing an existing array avoids that allocation but exposes the same array instance to the method body. Params collections may also allocate a temporary collection and copy elements into it, especially when the target type is an interface such as IEnumerable<T>. The overhead depends on the collection type and the number of arguments. In hot paths, that added allocation can be measurable.
Callers that already have an array can avoid allocation by using an array parameter directly. If you need to avoid heap allocation for inline calls, C# 13 supports params ReadOnlySpan<T>; in the inline case, the compiler can create a stack-based span rather than allocating a collection.
public static int Sum(params ReadOnlySpan<int> numbers) { int total = 0; foreach (int n in numbers) total += n; return total; }
This is the most allocation-friendly option, but it requires C# 13 and has the normal span limitations: a span cannot be stored in a regular class field or used across an await boundary.
Common Mistakes and Edge Cases
params is not automatically valid for every collection type. The parameter type must be constructible from a collection expression for the compiler and target framework you are using; otherwise, the method declaration is rejected. For array params, a caller can pass null, which makes the numbers parameter null rather than an empty array. If a method can receive null, handle it explicitly.
public static int Sum(params int[] numbers) { if (numbers == null) return 0; return numbers.Sum(); }
Overload resolution with params can also be surprising. Adding overloads that include a leading fixed parameter changes which candidate the compiler selects. For example, with both Sum(int a, params int[] rest) and Sum(params int[] all), a call such as Sum(1, 2) resolves to the two-parameter overload because its normal form is preferred over the expanded form of the other overload. That may not be what the caller intended.
Choosing Between params Array and Explicit Collection Parameters
params is the idiomatic choice when you expect callers to pass individual values. An explicit collection parameter is often clearer when callers usually already have a collection:
public static int SumList(List<int> numbers) { ... }
The tradeoff is implicit allocation. With params IEnumerable<int>, callers can write Sum(1, 2, 3) or Sum(myList), but the former may create a temporary collection. An array parameter is more compatible with older C# versions, and a ReadOnlySpan<T> parameter is the strongest choice when allocation must be minimized and the target project has C# 13 enabled. For most public APIs, array-based params remains the safest and most compatible option.