C# Params Multiple Arguments
Learn how the C# params keyword lets methods accept multiple arguments without explicit arrays, including syntax, overload resolution, null handling, allocation tradeoffs, and when to use collections instead.
When you want a C# method to accept a varying number of arguments, the params keyword is the standard tool. Instead of forcing callers to build an array or a list before every call, params lets them pass arguments directly. This article covers the practical syntax, behavior, and tradeoffs of using params in real code.
The Core Syntax: One Keyword, Many Arguments
A method that uses params declares a single array parameter, but callers can pass any number of individual arguments. The compiler collects those arguments into the array before the method body runs.
public static int Sum(params int[] numbers) { int total = 0; foreach (int n in numbers) { total += n; } return total; }
Callers can pass zero, one, or many integers:
int a = Sum(); // 0 int b = Sum(10); // 10 int c = Sum(1, 2, 3); // 6
The parameter is still an ordinary array; the method can inspect its Length property. The syntax at the call site avoids the boilerplate of new int[] { ... }.
The params keyword can appear only once in a method signature, and it must be the last parameter. That rule keeps the compiler unambiguous about where the variable part begins.
Calling with Arrays, Lists, or Existing Collections
params accepts an array directly, which is useful when a value is already stored as an array. A List<int> or any other IEnumerable<int> cannot be passed directly, because the compiler only converts individual arguments into the array—it does not iterate over a collection automatically.
int[] values = { 4, 5, 6 }; int d = Sum(values); // Works: passes the array reference List<int> list = new List<int> { 7, 8 }; // int e = Sum(list); // Compiler error: cannot convert from List<int> to int[]
To pass a list, call .ToArray() explicitly, which creates a new array and copies the elements.
int e = Sum(list.ToArray());
The same applies to LINQ results or any IEnumerable<int>. If you need to pass an arbitrary collection without the allocation cost, an overload or a different parameter type may be a better design.
Overloading with Regular Parameters
A common pattern is to provide an overload that takes individual typed parameters, then delegates to a params version. This is useful when you want to encourage a specific number of arguments while still supporting a variable list.
public static string Format(string template, params object[] args) { // Implementation using string.Format-like logic }
Overload resolution can be surprising when a method has both a params overload and a non-params overload. Consider:
public static void Log(string message) { } public static void Log(string format, params object[] args) { }
A call like Log("Hello") resolves to the single-parameter overload, not the params one, because the compiler prefers a normal-form match over an expanded params match. That is usually the expected behavior, but it is worth remembering when you expect a single argument to be treated as an array.
Handling Null and Empty Argument Lists
When a caller passes no arguments, the params array is an empty array, not null. That means a foreach loop or a Length check works safely without a null guard.
However, a caller can explicitly pass null as the array:
Sum(null);
That compiles because null is a valid int[]. Inside the method, the array reference is null, so a guard is sometimes necessary.
if (args == null) { return 0; // or throw ArgumentNullException }
The choice depends on whether a null argument indicates a caller bug or a legitimate empty state. For most APIs, it is safer to throw an ArgumentNullException with a clear parameter name.
Performance and Allocation Costs
The params keyword changes the call site. When you call Sum(1, 2, 3), the compiler generates an array containing those values. Calling with zero or one argument still creates an empty or single-element array unless the caller already supplies an array. The C# language does not generally elide that allocation.
The array is also a normal reference type. If you pass an existing array, the method receives the same array, not a copy. If the method mutates the array, callers see those changes.
static void Clear(params int[] numbers) { for (int i = 0; i < numbers.Length; i++) { numbers[i] = 0; } } int[] data = { 1, 2 }; Clear(data); // data is now { 0, 0 } because Clear received the same array reference
If your method needs to treat the arguments as immutable, copy them into a new array or document the mutation behavior.
Using Params for Format Strings and Logging
A classic use of params is to pass a format string and a variable set of values. This is how .NET's string.Format and many logging frameworks accept arguments.
public static void LogMessage(string format, params object[] parameters) { string message = string.Format(format, parameters); // Write to file or output }
Callers can write:
LogMessage("User {0} logged in at {1}", userId, time);
The downside is loss of type safety: any object can be passed, and if a placeholder references a missing parameter, an exception occurs at runtime. That is a reasonable tradeoff for flexible logging, but avoid it for APIs where compile-time checks are feasible.
Variant: Params with Generic Types
You can use params with a generic type, but the compiler infers the array element type from the arguments.
public static T FirstOrDefault<T>(params T[] items) { return items.Length > 0 ? items[0] : default; }
Calls like FirstOrDefault(1, 2) and FirstOrDefault("a", "b") work naturally. If arguments do not share a single inferred type, such as FirstOrDefault(1, "a"), type inference fails. You can specify an explicit type argument, such as FirstOrDefault<object>(1, "a"), or design the method with an object[] parameter if heterogeneous values are the goal.
Edge Cases and Common Pitfalls
A params parameter can receive zero arguments, but it must remain the last parameter. You cannot put an optional parameter after it; optional parameters before a params parameter are allowed.
Another common misconception is that an implementing method must repeat params when implementing an interface. Because params is not part of the C# method signature, a class can implement void Print(params object[] args) with void Print(object[] args). The implementation still satisfies the interface, but callers using the concrete type do not get variable-argument syntax unless the implementation also declares params.
When to Choose a Collection Parameter Instead
params is convenient, but it is not always the best API design. If a method operates on a large collection, requiring an explicit IEnumerable<T> or IReadOnlyList<T> communicates intent better and avoids the array allocation entirely.
| Criterion | params array | List or IEnumerable |
|---|---|---|
| Caller convenience | High | Lower (must build collection) |
| Allocation per call | Yes (array) | No if caller reuses collection |
| Type safety | Depends on element type | Stronger with generic constraints |
| Suitability | Small, fixed-format calls | Large or dynamic data sets |
Use params when a method naturally receives a small number of arguments that vary per call, such as string.Concat or a custom aggregation. Prefer a collection when the set of inputs is computed dynamically, grows unbounded, or must be processed incrementally.
In summary, params is useful for simplifying call-site code without requiring a new type. The main tradeoffs are the array allocation, weaker compile-time checking for object[], and the reference semantics of the passed array. Understanding these behaviors lets you use params where it adds clarity and choose a more explicit parameter model when needed.