Locale Functions and Clean Code Principles

Programming C#

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" ?

This is an excellent point about code quality and design principles. 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()
{
    // 20 lines of code here...
    
    // Local function adding more lines
    void LocalHelper()
    {
        // 10 more lines here...
    }
    
    // 20 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.