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

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.