Skip to content

C# Local Functions

Reading Mode

C# Local Functions are an interesting feature released with C# 7.0. C# local functions are methods declared in another method and run in the context of that method. They are methods that are used only by one other method and help to keep each method small and focused.

In this post we explore C# Local functions and learn how best to make use of them in .net core applications.

What are local functions in C#

Essentially C# local functions allow the specification of a functions within the body of another method or function and only run within the context of that method.

C# Local Functions are designed to be methods that are used by only one other method to help keep each method small and focused in order to reduce class bloat. Local functions can be defined in the body of any method, constructor or property.

Nested functions in JavaScript

If you are a familiar with JavaScript, you'll know doubt observe that this type of behaviour may be similar to nested functions.

The interesting thing about nested functions in JavaScript is their variable scoping rules : they can access parameters and variables of the function or functions that are nested within.

function hypotenuse(a,b){
  function square(x) { return x*x; }
  return Math.sqrt(square(a) * square(b));
}

Inner Functions in Python

The Python Programming language also has the concept of Inner Functions

def outer(num1):
    def inner_increment(num1):  # hidden from outer code
        return num1 + 1
    num2 = inner_increment(num1)
    print(num1, num2)
 
inner_increment(10)

In C# 6.0 developers could replicate similar functionality in making use of Func<T> delegate inside of a method.

public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
 
        public override string ToString()
        {
            Func<int, string> generateAgeSuffix = age => age > 1 ? "s" : "";
            return $"{Name} is {Age} year{generateAgeSuffix(Age)} old";
        }
    }

In c# 7.0 we can now replace this implementation as follows

public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
 
        public override string ToString()
        {
            return $"{Name} is {Age} year{generateAgeSuffix(Age)} old";
            
            // Define a local function:
            string GenerateAgeSuffix(int age)
            {
                return age > 1 ? "s" : "";
            }
            
           
        }
    }

Why Local Functions in C#

There are several advantages to local functions

Encapsulation

You use local functions to protect them from anything happening outside of the function, meaning that they are hidden from the global scope

Keeping it DRY

When writing functions you may possibly want to use the same block of code in numerous places. However, that block of code will only make sense in the context of the function, and will not be used anywhere else in your application.

It's not worth the administrative overhead of extracting that out its own function at the class level. Using a local function you can extract the logic for reuse within the function.

Closure and Factory Functions

This is probably the most important reason to use local functions. Most local functions may be just ordinary functions that merely happened to be nested inside another function.

However, the ideal case for local functions is to do with encapsulation of some behaviour and pass it around like any other object, and still have access to the context in which they were first declared.

What's a closure

A closure simply causes the inner function to remember the state of its environment when called. The closure closes the local variable on the stack and this stays around after the stack creation has finished executing.

Jon Skeet has a great blog post about the beauty of closures and I recommend reading his book C# in Depth

The use of closures and factory functions is the most common, and powerful, use for local functions. In most cases, when you see a decorated function, the decorator is a factory function which takes a function as argument, and returns a new function which includes the old function inside the closure.

C# Local Functions in action

We'll create contrived example of local functions. Just to illustrate their use.

A console application that generates some form of random hero name for a user.

 public class Program
    {
        static void Main(string[] args)
        {
           Console.WriteLine("What is your name ?");
           var line = Console.ReadLine();
           
           void PrintHeroName()     //Local function 
           {
               Console.WriteLine("{0} Your Hero name is {1}," , line, CreateHeroName(line));
           }
 
           string CreateHeroName(string name){        // Another local function
              var rnd = new Random();
              var hero = nhttps://garywoodfine.com/wp-admin/admin.php?page=qode_startit_theme_menuew List {
                   "Marvellous Monker",
                  "Slinky Dinky",
                  "Juicy Lucy",
                  "Hairy Gairy",
                  "Lurky Gurky",
                  "Gory Mauri",
                  "Sad Vlad"
              };
 
              int index =rnd.Next(hero.Count);
              return hero[index];
 
           }
 
          PrintHeroName();
       }
    }

There is nothing really complicated about this code. However, it does prove a couple of really important concepts of Local Functions.

  • Local Functions don't have to have a return type eg. void
  • Local Functions can have return types
  • Local functions can call each other
  • Local functions can accept parameters

Lets quickly add some additional logic, to cope with a user not liking his given Hero Name. We're not going win any clean code points here, but I just wanted to highlight some important functionality to do with variables.

I just want to highlight here that if you declare a variable within the root function, it is accessible and writable by the nested functions.

public class Program
    {
        static void Main(string[] args)
        {
           Console.WriteLine("What is your name ?");
           var line = Console.ReadLine();
           string heroName = string.Empty;   // Variable which has global scope within the function
           void PrintHeroName()     //Local function 
           {
               heroName = CreateHeroName(line);
                Console.WriteLine("{0} Your Hero name is {1}," , line, heroName);
                Console.WriteLine("Do you like your Hero Name ? (y/n) ");
           }
 
           string CreateHeroName(string name){        // Another local function
              var rnd = new Random();
              var hero = new List {
                  "Marvellous Monker",
                  "Slinky Dinky",
                  "Juicy Lucy",
                  "Hairy Gairy",
                  "Lurky Gurky",
                  "Gory Mauri",
                  "Sad Vlad"
              };
 
              int index =rnd.Next(hero.Count);
              return hero[index];
 
           }
 
          PrintHeroName();
    
        while((Console.ReadLine().ToUpper() =="N"))
        {
            PrintHeroName();  
         }
       
           Console.WriteLine("Great!  Now go do some good deeds {0}", heroName);
       }
    }

Run the application and it will work.

The issue with the code here is that no support team will thank you for writing this spaghetti code!

We've utilised some great functionality in C#, but have only leveraged it to create a support nightmare.

Lets refactor the code and explore what else we can do with local functions.

We'll make use of events and even look at creating a self cleaning delegate , which previously required a lot of code trickery, but now is a relatively trivial task in C#

public class Program
    {
        private static event EventHandler hero;
        static void Main(string[] args)
        {
 
            string userName = string.Empty;
            string heroName = string.Empty;
 
            void Intro()
            {
                Console.ForegroundColor = ConsoleColor.DarkCyan;
                Console.WriteLine("Welcome to the Hero Name Generator Application :");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Please Enter your Name : ");
                Console.ForegroundColor = ConsoleColor.White;
                userName = Console.ReadLine();
 
                //Self Cleaning Delegate for fun
                hero += PrintUserNameEvent;
                hero?.Invoke(null, EventArgs.Empty);
                hero -= PrintUserNameEvent;
 
 
            }
 
            void PrintUserNameEvent(object sender, EventArgs e)
            {
                while(true)
                {
                     GetHeroName(userName);
                    var ans = Console.ReadLine();
                    if(ans != null && ans.Trim().Equals("y", StringComparison.CurrentCultureIgnoreCase))
                    {
                        break;
                    }
                }
               
            }
 
            void GetHeroName(string name)     //Local function 
            {
                heroName = GenerateHeroName();
                Console.ForegroundColor = ConsoleColor.DarkMagenta;
                Console.WriteLine("{0} Your Hero name is {1},", name, heroName);
                 Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Do you like your Hero Name ? (y/n) ");
            }
 
            string GenerateHeroName()
            {        // Another local function
                var rnd = new Random();
                var hero = new List {
                  "Marvellous Monker",
                  "Slinky Dinky",
                  "Juicy Lucy",
                  "Hairy Gairy",
                  "Lurky Gurky",
                  "Gory Mauri",
                  "Sad Vlad"
              };
 
                int index = rnd.Next(hero.Count);
                return hero[index];
            }
 
            Intro();
            Console.WriteLine("Great!  Now go do some good deeds {0}", heroName);
 
        }
    }

Important considerations of C# local functions

  • Scope of Local functions are only within the method they are defined and they should not contain system-wide logic.
  • Local functions are not visible to other classes
  • They are unit tested with methods that own them.
  • Use local functions only if treally needed.
  • over-use of local functions may lead to long methods that are hard to test.

Conclusion

It may not be immediately apparent in the above contrived examples, but local functions are extremely useful in solving computer science algorithm challenges.

Although it may be possible to make use of Lambda expression to do similar types of tasks there are a number of reasons to prefer using local functions instead of defining and calling lambda expressions.

For more info on this check out the Microsoft article - Local functions compared to Lambda expressions

Bill Wagner blog post : C# 7 Feature Proposal: Local Functions is also worth reading for further background on this great language feature.

Gary Woodfine
Latest posts by Gary Woodfine (see all)