Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Technical Blog

Putting the Fun in C# Local Functions

Rate me:
Please Sign up or sign in to vote.
4.81/5 (14 votes)
2 Mar 2020CPOL3 min read 16.3K   9   14
C# local functions and how they can be used to make code more readable
Many popular languages support the use of local functions and in C# 7, support for them was announced with relatively little fanfare. As someone that would consider themselves a C# power-user, I seldom took advantage of the feature until I realized just how much it could help with making code more readable, specifically in the context as a replacement for comments/hacks, unit tests, and in general, just to clean things up.

Putting the Fun in C# Local Functions

What Are Local Functions Exactly?

Local functions are private methods of a type that are nested in another member. They can only be called from their containing member. Local functions can be declared in and called from:

  • Methods, especially iterator methods and async methods
  • Constructors
  • Property accessors
  • Event accessors
  • Anonymous methods
  • Lambda expressions
  • Finalizers

As with most things, sometimes, it's easier to just show you what a local function looks like:

C#
public static IEnumerable<Address> SanitizeAddresses(List<Address> addresses)  
{
      foreach(Address address in addresses) 
      {
           yield return Anonymize(address);
      }

      // This is a local function
      Address Anonymize(Address address) { ... }
}

Cleaning Up Comments with Local Functions

Putting the Fun in C# Local Functions

One of the first use cases that comes to mind that functions can help alleviate is any pesky sanitation or business logic rules, particularly those around string manipulation, etc. If you've worked in enough business applications, you've undoubtably seen something terrible with some massive comment as to why it's being done:

public static User ProcessUser(User user)  
{
      // All names must conform to some crazy Dr. Seuss-eqsue rhyming scheme
      // along with every other character being placed by its closest numerically
      // shaped equivalent
      var seussifyExpression = new Regex("...");
      user.Name = seussifyExpression.Replace(user.Name, "...");
      user.Name = user.Name
                      .Replace(..., ...)
                      .Replace(..., ...)
                      .Replace(..., ...);

      // Other processes omitted for brevity

      return user;
}

As you can see here, we have a series of chained replacements, some relying on strings, and others relying on regular expressions, which can make a method pretty clunky, especially if there are multiple operations to perform. Now, this is where you can define a local function to encapsulate all this business logic to replace your crazy comment:

public static User ProcessUser(User user)  
{
      SanitizeName(user)

      // Other processes omitted for brevity

      return user;

      void SanitizeName(User user)
      {
          var seussifyExpression = new Regex("...");
          user.Name = seussifyExpression.Replace(user.Name, "...");
          user.Name = user.Name
                          .Replace(..., ...)
                          .Replace(..., ...)
                          .Replace(..., ...);

          return user;
      }
}

You could easily name your local function whatever you like, even ApplyBusinessLogicNamingRules() and include any necessary comments for reasoning that you'd like within there (if you absolutely need to answer why you are doing something), but this should help the rest of your code tell you what it's doing without a comment explicitly writing it all out.

Going All Reading Rainbow with Local Functions

Putting the Fun in C# Local Functions

If readability isn't the single most important thing about code, then it's damn close to the top.

LINQ is another popular area that local functions can assist with, especially if you have to do any type of crazy filtering logic over a series of records. You can define a series of local functions that can cover each step of your filtering process (or any process really), and more easily reason about your code from a readability perspective:

public List<int> FindPrimesStartingWithASpecificLetter(List<int> numbers, int startingDigit)  
{
    return numbers.Where(n => n > 1 && Enumerable.Range(1, n).Where
                        (x => n % x == 0).SequenceEqual(new [] {1, n }))
                  .Where(n => $"{n}".StartsWith($"{startingDigit}"));
}  

While succinct, it doesn't exactly read well. Let's take a gander at what it looks like after rubbing some local functions on it:

public List<int> FindPrimesStartingWithASpecificLetter(List<int> numbers, int startingDigit)  
{
    return numbers.Where(x => IsPrime(x) && StartsWithDigit(x, startingDigit));

    bool IsPrime(int n) => return n > 1 && Enumerable.Range(1, n).
                           Where(x -> n % n == 0).SequenceEqual(new [] { 1, n }));
    bool StartsWithDigit(int n, int startingDigit) => return $"{n}".StartsWith
                        ($"{startingDigit}");
}  

As you can see, local functions are assisting with wrapping up all the ugly/nasty logic within their own tiny little functions. This is a really trivial case, but as you might imagine if you have lines-upon-lines of code that isn't touching anything outside of one method, it's likely a solid candidate for a local function.

Testing, Testing, Lo-ca-lly!

Putting the Fun in C# Local Functions

If you've spent any amount of time writing tests, either unit or integration, you are probably familiar with the fabled 'Arrange-Act-Assert` pattern, which is used to separate each piece of functionality when testing a given piece of code as follows:

  • Arrange all necessary preconditions and inputs.
  • Act on the object or method under test.
  • Assert that the expected results have occurred.

As you might imagine, this could lend itself to the pattern quite well for complex test cases:

public void IsThisAnArrangeActAssertLocalFunction()  
{
     Arrange();
     Act();
     Assert();

     void Arrange() { ... }
     void Act() { ... }
     void Assert() { ... }
}

Is it practical? Does it fit all use cases? Is it something that you'd ever find yourself using? The answers to all of these might be an overwhelming no, but it does seem like a scenario where local functions could play a role.

Choose Your Own Adventure

Putting the Fun in C# Local Functions

Local functions present a few interesting options that fit some scenarios better than others. As a replacement for large comments or very messy business logic - absolutely. In unit tests or little one liners - probably not. With most new features, especially those that are sugary, it's really up to you and your team to see if they work for you. While they may seem appealing in some situations, they also seem ripe for abuse, potentially cluttered methods, and other issues that would completely defeat the purpose of using them in the first place.

So, if you choose to go down this road of local functions, proceed with care.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
An experienced Software Developer and Graphic Designer with an extensive knowledge of object-oriented programming, software architecture, design methodologies and database design principles. Specializing in Microsoft Technologies and focused on leveraging a strong technical background and a creative skill-set to create meaningful and successful applications.

Well versed in all aspects of the software development life-cycle and passionate about embracing emerging development technologies and standards, building intuitive interfaces and providing clean, maintainable solutions for even the most complex of problems.

Comments and Discussions

 
QuestionTesting Pin
Emile van Gerwen9-Mar-20 5:42
Emile van Gerwen9-Mar-20 5:42 
Since you mentioned testing... how would you test local functions?

Emile.
QuestionGood food for thought Pin
James Lonero3-Mar-20 10:03
James Lonero3-Mar-20 10:03 
QuestionNIce... Pin
  Forogar  2-Mar-20 5:05
professional  Forogar  2-Mar-20 5:05 
BugCompilation error Pin
Richard Deeming2-Mar-20 0:58
mveRichard Deeming2-Mar-20 0:58 
GeneralRe: Compilation error Pin
George Swan2-Mar-20 4:39
mveGeorge Swan2-Mar-20 4:39 
GeneralRe: Compilation error Pin
Richard Deeming2-Mar-20 4:42
mveRichard Deeming2-Mar-20 4:42 
GeneralRe: Compilation error Pin
George Swan2-Mar-20 4:45
mveGeorge Swan2-Mar-20 4:45 
GeneralRe: Compilation error Pin
Richard Deeming2-Mar-20 4:48
mveRichard Deeming2-Mar-20 4:48 
GeneralRe: Compilation error Pin
George Swan2-Mar-20 6:10
mveGeorge Swan2-Mar-20 6:10 
GeneralRe: Compilation error Pin
Richard Deeming2-Mar-20 7:03
mveRichard Deeming2-Mar-20 7:03 
GeneralRe: Compilation error Pin
George Swan2-Mar-20 8:31
mveGeorge Swan2-Mar-20 8:31 
GeneralRe: Compilation error Pin
rhyous3-Mar-20 9:24
rhyous3-Mar-20 9:24 
GeneralRe: Compilation error Pin
George Swan3-Mar-20 10:02
mveGeorge Swan3-Mar-20 10:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.