Local Functions and params
A local function is a method declared inside another method and callable only from it; params lets one parameter soak up any number of arguments.
int Outer(params int[] values) { ... int Inner(int v) => v; }Sometimes a method needs a small helper that nothing else should ever call. A local function is a method declared inside another member. It can only be called from that member, so its name never leaks and no one can call it by mistake.
How local functions behave
It is written like any method, but with no access modifier; adding private is error CS0106. It may be declared before or after the statements that call it, including after return, so helpers can sit at the bottom out of the way. It can read the enclosing method's variables and parameters. Marking it static (C# 8.0 and later) forbids that, which prevents accidental dependence on surrounding state and lets the compiler avoid an allocation.
When it beats a private helper
Use a local function when the helper only makes sense inside one method, especially if it needs that method's variables. Use a private method of the type when two members need it, or when it deserves its own tests.
params
params on the last parameter lets the caller pass loose arguments instead of building a collection. Up to C# 12 that parameter had to be an array. C# 13 widened it to recognised collection types including Span<T>, ReadOnlySpan<T> and interfaces such as IEnumerable<T>. Only one params parameter is allowed, and it must come last.
What trips people up
A params method can also be called with a whole collection. With params object[], passing an int[] makes the array a single element rather than the list of items.
Example
// params lets the caller pass any number of arguments.
// An array is a fixed-length row of values of one type. Stage 3 covers arrays.
int Total(params int[] numbers)
{
int sum = 0;
foreach (int number in numbers)
{
sum += Weight(number);
}
return sum;
// A local function: declared inside Total, callable only from Total.
// It may sit after the return statement. static means it cannot read
// Total's variables, so nothing has to be allocated to reach them.
static int Weight(int value) => value < 0 ? 0 : value;
}
Console.WriteLine(Total(1, 2, 3)); // three loose arguments
Console.WriteLine(Total(10, -5, 40)); // the negative one counts as 0
Console.WriteLine(Total()); // no arguments: an empty array
Console.WriteLine(Total(new int[] { 20, 22 })); // same as Total([20, 22])
// Output:
// 6
// 50
// 0
// 42When to use it
- Keep a three-line helper next to the only method that uses it instead of adding it to the type.
- Let a helper use the enclosing method's variables without passing them as parameters.
- Write a logging or sum method callers invoke with any number of values and no array.
More examples
A local function that reads the enclosing method
This is the case where a local function clearly wins: the helper needs a value the enclosing method already has. Keeping it local avoids threading that value through an extra parameter.
string Format(int[] amounts)
{
string currency = "GBP";
// Not static: it reads currency from the method around it.
// A private helper would need currency passed in as a parameter.
string One(int amount) => $"{amount} {currency}";
string line = "";
foreach (int amount in amounts)
{
line += One(amount) + " ";
}
return line.TrimEnd();
}
Console.WriteLine(Format(new int[] { 5, 10 }));
// Output:
// 5 GBP 10 GBPRecursion inside a method
A local function has a name from the moment it is compiled, so it can call itself directly. The public method stays the simple two-argument one while the recursive shape with extra arguments stays hidden.
int Fibonacci(int n)
{
return Step(n, 0, 1);
// A local function can call itself.
static int Step(int left, int a, int b) => left == 0 ? a : Step(left - 1, b, a + b);
}
Console.WriteLine(Fibonacci(1));
Console.WriteLine(Fibonacci(10));
// Output:
// 1
// 55params beyond arrays, from C# 13
Callers see no difference from params int[], but a frequently called method avoids an allocation per call. Before C# 13 this had to be written as params int[].
// C# 13 and later: params is no longer limited to array types.
// A ReadOnlySpan<int> parameter needs no array allocated per call.
int Max(params ReadOnlySpan<int> values)
{
if (values.Length == 0)
{
return 0;
}
int best = values[0];
for (int i = 1; i < values.Length; i++)
{
if (values[i] > best)
{
best = values[i];
}
}
return best;
}
Console.WriteLine(Max(3, 42, 17));
Console.WriteLine(Max());
// Output:
// 42
// 0
Discussion