How to Use Local Functions in C# Methods
Local functions in C# allow developers to declare methods directly inside the body of another method, constructor, or property accessor. This article explains how to define and use local functions, covers key features such as closures and static local functions, and highlights common use cases like argument validation in asynchronous code and iterators.
What Is a Local Function?
Introduced in C# 7.0, a local function is a private helper method scoped entirely within its containing member. It cannot be called from outside the parent method, making it ideal for encapsulating logic that is only relevant in one specific place.
Basic Syntax and Implementation
You define a local function inside an enclosing method using the same
syntax as a standard method. Local functions can be placed anywhere
within the enclosing block, even after a return
statement.
public void ProcessOrder(int orderId)
{
// Call the local function
PrintReceipt(orderId);
// Local function definition
void PrintReceipt(int id)
{
Console.WriteLine($"Receipt for Order #{id}");
}
}Accessing Enclosing Scope (Closures)
Local functions can read and modify variables defined in the enclosing method without passing them explicitly as parameters.
public int CalculateDiscountedTotal(int[] prices, double discountRate)
{
int total = 0;
foreach (var price in prices)
{
total += ApplyDiscount(price);
}
return total;
// Accesses 'discountRate' directly from the parent method's scope
int ApplyDiscount(int price)
{
return (int)(price * (1 - discountRate));
}
}Static Local Functions
Starting in C# 8.0, you can add the static modifier to a
local function. A static local function cannot capture
local variables or the this reference from the enclosing
scope. This prevents unintentional memory allocations and ensures
variables are passed explicitly.
public int ComputeSum(int a, int b)
{
return Add(a, b);
// Static prevents capturing variables from ComputeSum
static int Add(int x, int y) => x + y;
}Common Use Cases
1. Eager Argument Validation in Async Methods
In standard async methods, exceptions thrown during
argument validation are deferred until the returned Task is
awaited. Using a local function allows immediate parameter validation
before running the asynchronous operation.
public Task<string> FetchDataAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentException("URL cannot be empty.", nameof(url));
}
return ImplAsync(url);
static async Task<string> ImplAsync(string targetUrl)
{
using var client = new HttpClient();
return await client.GetStringAsync(targetUrl);
}
}2. Eager Argument Validation in Iterator Methods
Methods using yield return do not execute until the
collection is enumerated. A local function ensures validation runs
immediately upon calling the method.
public IEnumerable<int> GenerateSequence(int start, int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative.");
}
return Iterator();
IEnumerable<int> Iterator()
{
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
}3. Recursive Helper Methods
Local functions simplify recursive algorithms by encapsulating the recursion within the main method and eliminating the need for a separate, private class-level method.
public int Factorial(int n)
{
if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));
return Calculate(n);
int Calculate(int value)
{
return value <= 1 ? 1 : value * Calculate(value - 1);
}
}Local Functions vs. Lambda Expressions
While similar to lambda expressions assigned to a Func
or Action, local functions offer distinct advantages:
- Performance: They avoid heap allocations associated with delegate instances.
- Flexibility: They support
ref,out, andinparameters, generic types, and theparamskeyword. - Readability: They use standard method syntax and appear with their actual names in compiler stack traces, making debugging easier.