Local functions are methods of a type that are nested in another member. They can only be called from their containing member. Introduced quite some time ago (March 2017) with version 7 of the C# language they serve several important purposes and offer distinct advantages:
Encapsulation and Scope Control
public int ProcessData(int[] numbers)
{
// Local function - only visible within ProcessData
int CalculateSum(int start, int end)
{
int sum = 0;
for (int i = start; i < end; i++)
sum += numbers[i];
return sum;
}
return CalculateSum(0, numbers.Length);
}
Better Performance with Iterator Methods
public IEnumerable<int> Filter(int[] numbers)
{
// Local function avoids state machine allocation
bool IsValid(int num)
{
return num > 0 && num < 100;
}
foreach (var num in numbers)
{
if (IsValid(num))
yield return num;
}
}
Avoiding Closure Allocations
// Without local function (creates closure)
public async Task<int> BadExample(int value)
{
int multiplier = 10;
return await Task.Run(() => value * multiplier);
}
// With local function (no closure allocation)
public async Task<int> GoodExample(int value)
{
int multiplier = 10;
int Multiply()
{
return value * multiplier;
}
return await Task.Run(Multiply);
}
Clear Intent and Readability
public double CalculateArea(double radius)
{
double CalculateCircleArea()
{
const double pi = 3.14159;
return pi * radius * radius;
}
if (radius <= 0)
throw new ArgumentException("Radius must be positive");
return CalculateCircleArea();
}
Access to Local Variables
public void ProcessOrder(Order order)
{
var tax = 0.2m;
var discount = 0.1m;
decimal CalculateFinalPrice(decimal price)
{
return price * (1 + tax) * (1 - discount);
}
order.FinalPrice = CalculateFinalPrice(order.BasePrice);
}
Key Benefits
- Better encapsulation
- Improved performance (avoiding closures)
- Clearer scope
- Access to local variables
- Better readability for complex algorithms
- Reduced memory allocations
- More maintainable code
When to Use
- Helper functions needed only within a specific method
- Complex algorithms that can be broken down
- Iterator methods where performance is critical
- Avoiding closure allocations in async code
- When the function logic is tightly coupled to the containing method
Conclusion
Local functions provide a clean way to organize code while potentially improving performance and maintaining better encapsulation.
What about Clean Code Principles?
But aren't locale functions a violation of Clean Code rulings like f.e. "a function should not be larger than X (f.e. 20 or 30) lines of code" ?
Good question. The "function size" rule and local functions can indeed seem contradictory at first glance. Here's how to think about it:
Bad usage example that is violating Clean Code Principles:
public void LargeMethodWithLocalFunction()
{
// 10 lines of code here...
// Local function adding more lines
void LocalHelper()
{
// 10 more lines here...
}
// 10 more lines of code...
LocalHelper();
// Even more code...
}
Good usage example that is following Clean Code Principles:
// Case 1: Algorithm-specific helper
public decimal CalculateDiscount(Order order)
{
decimal ApplySeasonalMultiplier(decimal amount)
{
return DateTime.Now.Month == 12 ? amount * 1.2m : amount;
}
var baseDiscount = order.Value * 0.1m;
return ApplySeasonalMultiplier(baseDiscount);
}
// Case 2: Iterator optimization
public IEnumerable<int> FilterNumbers(IEnumerable<int> numbers)
{
bool IsValid(int n) => n > 0 && n < 100;
foreach (var num in numbers)
if (IsValid(num))
yield return num;
}
Guidelines for Clean Local Functions
- Keep Both Functions Small
- Main method should still follow the 20-30 lines rule
- Local function should be small and focused
- Combined they should still maintain reasonable length
- Single Responsibility
- Local function should handle one specific sub-task
- Should be tightly coupled to the parent method's purpose
Appropriate Use Cases
✅ GOOD:
- Iterator methods (performance optimization)
- Small algorithm-specific helpers
- Avoiding closure allocations
- Validation logic specific to the method
❌ BAD:
- Complex business logic that should be its own method
- Reusable functionality that could benefit other methods
- Logic that makes the parent method too large
When to Refactor to Regular Methods:
// Before: Large local function
public void ProcessOrder(Order order)
{
void ValidateAndCalculate()
{
// 20+ lines of validation and calculation
}
ValidateAndCalculate();
}
// After: Proper separation
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateOrderTotals(order);
}
private void ValidateOrder(Order order) { /*...*/ }
private void CalculateOrderTotals(Order order) { /*...*/ }
Best Practices Summary
- Use local functions for truly local concerns
- Keep both parent and local functions small
- If the local function is reusable, make it a regular method
- If the combination violates size rules, refactor to separate methods
- Use local functions primarily for performance optimization and scope control
The key is balance: Local functions should enhance readability and performance without violating clean code principles. If they make the code harder to understand or maintain, that's a sign they should be refactored into regular methods.