How to use closures in C#

Take advantage of closures in C# — including anonymous methods, delegates, and lambda expressions — to make your code robust, efficient, readable, and easier to maintain.

Closures are often associated with functional programming languages. Closures connect a function to its referencing environment, allowing the function to access non-local variables. In C#, closures are supported using anonymous methods, lambda expressions, and delegates.

I have discussed anonymous methods and lambda expressions in previous articles. So what’s a delegate? A delegate is a type-safe function pointer that can reference a method that has the same signature as that of the delegate. Delegates are used to define callback methods and implement event handling.

This article talks about how we can work with closures using anonymous methods, lambda expressions, and delegates in C#. To work with the code examples provided in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.

Create a console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Click Create.

We’ll use this project to illustrate the use of anonymous methods, lambdas, and delegates as closures in the subsequent sections of this article.

A closure as a first-class function in C#

A closure is defined as a first-class function containing free variables bound in the lexical environment. The C# programming language treats the first-class function as though it were a first-class data type. This means that you can assign the function to a variable, invoke it, or pass it around much the same way you work with any other first-class data type.

A closure is a particular type of function that is intrinsically linked to the environment in which it is referenced. As a result, closures can use variables pertaining to the referencing environment, despite these values being outside the scope of the closure.

Simple closure examples in C#

You can write a closure using an anonymous method as shown in the code snippet given below.

Func<string, string> someFunc = delegate (string someVariable)
{
   return "Hello World!";
};

Alternatively you can create a closure using a lambda function as shown in the code snippet below.

Func<string, string> someFunc = someVariable => "Hello World!";

Note that both of the above code snippets create a method that accepts a string as a parameter and returns another string. Here’s how you can invoke either of these closures we just created:

string str = someFunc("This is a demo");

Let’s look at another example. The code snippet given below creates an integer value in a non-local variable named x.

int x = 10;
Action closure = delegate
{
     Console.WriteLine("The value of the non-local variable x is: {0}", x);
};
closure();

Here’s how you can do exactly the same thing using a lambda expression:

int x = 10;
Action closure = () =>
{
    Console.WriteLine("The value of the non-local variable x is: {0}", x);
};
closure();

In both cases, the output will appear exactly as displayed in Figure 1 below.

closures csharp 01 IDG

Figure 1: Our simple closure in action!

Closures capture variables, not values

Because a closure is bound to the environment in which it is declared, it is able to reference out-of-scope variables and objects from within its body. Here is an example that illustrates this:

int x = 10;
Action a = delegate { Console.WriteLine($"The value of x is: {x}"); };
a();

When you execute the above code, the output will appear at the console window as shown in Figure 2.

closures csharp 02 IDG

Figure 2: A closure can reference non-local variables and objects within its body.

The following code snippet shows that the anonymous method is bound to variables in the parent method body, not values.

int x = 10;
Action a = delegate { Console.WriteLine($"The value of x is: {x}"); };
x = 100;
a();

When you execute the above code, the output will appear as shown in Figure 3. Note that the anonymous function returns 10, not 100.

closures csharp 03 IDG

Figure 3: A closure is bound to variables, not values, of the parent method body.

How do C# closures work?

When the C# compiler detects a delegate that forms a closure that is moved beyond the current scope, the delegate and its associated local variables are promoted to a compiler-generated class. From there, it just takes a little compiler magic to move between instances of the compiler-generated class, ensuring that each time the delegate is invoked, a function on this class is called. Once there are no longer any references to the instance of this class, the instance is garbage collected by the GC much the same way other instances are collected.

Below is an example of a compiler-generated class created on compilation of a code snippet that contains closures.

[CompilerGenerated]
 private sealed class <>c__DisplayClass0_0
    {
        public int x;
        internal void <M>b__0()
        {
            Console.WriteLine(string.Format("The value of x is: {0}", x));
        }
    }

For a lambda to remain “callable,” the variables it references must survive even after the function in which they have been defined has finished executing. To achieve this, C# draws on classes. So, when a lambda function accesses a variable that is defined inside a function, that variable is extracted and placed inside a new class generated by the compiler. That’s exactly how a closure works!

Closures originated in the world of functional programming but they have found a place in object-oriented programming languages as well. Closures don’t inherently provide composability — all they do is make it easier to implement delegates. I’ll have more to say about closures in future posts here.

Copyright © 2021 IDG Communications, Inc.