Using C#7 Local Functions

A little bit of local functions goes a long way to improving readability of code in many ways

After I wrote a post recently that covered the Tuple C# 7.0 language feature it got me thinking about all the other language enhancements that have been added in C# 7.0 and its three subsequent point releases and how little I've actually familiarised myself with them, let alone used any of them. As a great way to learn about something is to teach it / describe it to others, I've settled on a periodic series of posts to cover the added language features as a way to grow what I know and hopefully share some of that.

What are local functions?

Well, there's a detailed explanation that comes with many different examples at docs.microsoft.com but in brief, a local function is a function that can only be called inside the function it's declared in. Yeah, that sounds a bit twisty doesn't it! How about I put it another way....

If you're writing a method and discover that you need the same chunk of code twice within it, you'd usually end up extracting that into a separate private method (you definitely wouldn't copy and paste it into your method twice, right?!) which you'd then call from both places. This private method would then be usable from anywhere inside the class and would require all variables that need to be used to be either (a) passed in, or (b) promoted to globals within the class so they can be accessed. Local functions remove all these issues, because:

  • Local functions are only callable from within the method they're declared in. If your local function does something you absolutely don't want other parts of the class to be doing, that's great - it's not accessible from elsewhere
  • Local functions have access to all local variables and parameters of their parent method. There's no need to promote values to globals or have a long and mistake prone method signature

Using a local function

Say for a moment that we were writing a function that returns the sum of the values of each digit in a number, i.e. 123 gives 1+2+3 = 6, but we wanted to go one better and get the total for multiple values (so {123, 456} = (1+2+3) + (4=5+6), with a local function that takes advantage of the fact that it can capture variables from its parent function and use them could give us this:

public int GetSumOfNSmallestValuesWithCapturing(IEnumerable<int> values)
{
// Error handling and validation skipped
var accumulator = 0; foreach (var value in values.OrderBy(num => num)) { AccumulateSumOfNumbers(value); } return accumulator; void AccumulateSumOfNumbers(int value) { var valueAsString = value.ToString(); foreach (var number in valueAsString) { accumulator += Convert.ToInt32(number) - 48; } } }

There's not much point, in this example, in having the local function present from a code re-use perspective as it's not being re-used, nor is there anything particularly "I wouldn't want another method to use this" in the code. It does have the advantage of simplifying the main code flow though - if you're reading and comprehending the GetSumOfNSmallestValuesWithCapturing method, it's a lot easier to reason about as the details of accumulating are separate and can thus be looked at separately.

Aside: The '- 48' is a bit of a trick (a magic number, arrrgh!) as I'm relying on the fact that each of the individual digits in the number when represented as a char (which is what you get when you iterate over a string) will have a numeric value between 48, for 0 and 57, for 9 - thus deducting 48 gives the value between 0 and 9 for each letter/digit.

How about if we wanted to take the list of numbers and distill it down until the result was a number in the range 1..9? This is a better example of the code re-use that a local function can give:

public int GetSummedSumWhenTheResultIsBetweenOneAndNine(IEnumerable<int> values)
{
    var accumulator = 0;

    foreach (var value in values.OrderBy(num => num))
    {
        AccumulateSumOfNumbers(value);
    }

    while(accumulator > 9)
    {
        var oldAccumulatorValue = accumulator;
        accumulator = 0;
        AccumulateSumOfNumbers(oldAccumulatorValue);
    }

    return accumulator;

    void AccumulateSumOfNumbers(int value)
    {
        var valueAsString = value.ToString();
        foreach (var number in valueAsString)
        {
            accumulator += Convert.ToInt32(number) - 48;
        }
    }
}

That shows the way the local function can be re-used more clearly, it also shows that sharing the local from the parent function can lead to code that's a bit twistier as I've had to introduce the variable oldAccumulatorValue to allow me to get a "fresh" result from each call to AccumulateSumOfNumbers each time it's called in the while loop. If this were production code, I'd probably move the "complexity" into the AccumulateSumOfNumbers local function:

public int GetSummedSumWhenTheResultIsBetweenOneAndNineWithoutCapturing(IEnumerable<int> values)
{
    var accumulator = 0;

    foreach (var value in values.OrderBy(num => num))
    {
        accumulator += AccumulateSumOfNumbers(value);
    }

    while (accumulator > 9)
    {
        accumulator = AccumulateSumOfNumbers(accumulator);
    }

    return accumulator;

    int AccumulateSumOfNumbers(int value)
    {
        var accumulatedValue = 0;
        var valueAsString = value.ToString();
        foreach (var number in valueAsString)
        {
            accumulatedValue += Convert.ToInt32(number) - 48;
        }

        return accumulatedValue;
    }
}

With that done the code in the main function is once again about as simple as it can be and the AccumulateSumOfNumbers local function is now entirely self-contained. 

I can think of several places in codebases that I'm responsible for where using local functions will allow me to simplify some code (by hiving code off into a local that would've been tricky to do with a run-of-the-mill separate function) or make its intent clearer.

About Rob

I've been interested in computing since the day my Dad purchased his first business PC (an Amstrad PC 1640 for anyone interested) which introduced me to MS-DOS batch programming and BASIC.

My skillset has matured somewhat since then, which you'll probably see from the posts here. You can read a bit more about me on the about page of the site, or check out some of the other posts on my areas of interest.

No Comments

Add a Comment