What are Closures in C#?
Closures are commonly associated with functional programming languages, where they connect a function to its surrounding environment, thereby allowing access to variables outside the scope of the function.
Closures are available in C# via delegates. With that basic knowledge, let us now dive deep and understand everything about closures in this post.
Table of contents
- What are closures in C#?
- Using Closures in C#
- Closure and out-of-scope variables
- Closure captures variable, not value
What are closures in C#?
Functional programming languages are a common playground for lexical closures. A closure is a type of function that is tied to the code that defines it. This enables closure functions to use variables from the reference environment, despite the fact that these values are outside the scope of the closure. The external variables that the function uses are "closed over" when it is formed. This means that they are bound to the closure function in a way that makes them accessible.
Using Closures in C#
Closures can be constructed in C# using anonymous methods or lambda expressions, depending on the version of the.NET framework you're working with. The variables that the function will utilize (that are outside of its visible scope) are copied and stored with the closure's code when you construct it. When the delegate is called, they can use them. When employing such delegates, you get the luxury of flexibility, but it also presents a risk of various unanticipated issues. These will be discussed later in this text. Let's now have a look at a simple closure first.
We generate an integer value in the "nonLocal" variable in the code below. The second statement creates an instance of the Action delegate, which outputs a message based on the integer variable's value. Finally, we run the delegate and wait for the message to arrive. Whether you're used to closures or not, the results are likely to be exactly as what you expect.
With a lambda expression, we can achieve the same result.
Closure and out-of-scope variables
Since the capture of variables by the closure is not quite obvious in the anonymous method or lambda expression instances, the results are as you might expect. We can make this more obvious by modifying the delegate's scope.
Consider the code below. The closure is stored in a class-level Action variable in this case. Before executing the closure, the main function calls the setclosure method to initialize it. It's crucial to use the setclosure function. The integer variable is created, initialized, and then utilized within the closure, as you can see. This integer variable is no longer in scope when the setclosure procedure is completed. However, we continue to invoke the delegate after this occurs. The end effect is no longer so evident.
Is it going to compile and run properly?
Will there be an error if you try to access an out-of-scope variable?
Let’s find out.
As you can see, we got the same result as in the first example. This is an example of closure in action. The delegate code seized, or "closed over," the "nonLocal" variable, allowing it to remain in scope beyond the customary bounds. It will, in reality, be available until there are no more references to the delegate.
Although we've seen closures work, C# and the.NET framework don't truly support them. What truly happens is that the compiler does some behind-the-scenes work. The compiler creates a new, hidden class that contains the non-local variables and the code you include in the anonymous function or lambda expression when you build your project. Non-local variables are expressed as fields in the code, which is included in a method. When the delegate is run, the method of this new class is invoked.
A simple closure's automatically produced class looks like this:
Closure captures variable, not value
When closures are defined, certain programming languages save the values of variables used in those closures. The variables are captured by C#. This is a crucial distinction since it can cause values to shift over the scope border. Consider the following code as an example. We're going to make a closure that outputs our well-known mathematical equation. The integer variable's value is 1 when the delegate is declared. The variable's value changes to 5 after the closure is defined but before it is executed.
Since the non-local variable has a value of 1 at the time the closure is constructed, you might anticipate the output to be "1 + 1 = 2." This is exactly what would happen with various programming languages. However, since the variable is recorded, any change in its value has an impact on the closure's execution. The final result is as follows:
Changes to a non-local closure variable are also passed back and forth. The delegate in the following code modifies the value before the declaring code displays it. Despite being in a different scope than the closure, the change is apparent in the external code.
Variable capture exposes our programs to the risk of introducing unanticipated bugs. Another example might be used to highlight this issue. We're going to use closures in a more common scenario this time: multi-threaded or parallel programming. A for loop is seen below, which creates and starts five new threads. Before displaying the value of the loop's control variable, each stops briefly. If the control variable's value was captured, the numbers one through five would be written to the console, though potentially not in the correct order. We see the final value of 6 for each thread since the variable is bound to the closure and the loop completes before the threads produce their messages.
Fortunately, this type of issue is simple to resolve once you realize that variables, not values, are being collected. For each iteration of the loop, we simply need to introduce a new variable instance. This can be declared in the body of the loop and given the control variable's value. When the loop ends, the temporary variable would normally go out of scope, but the closure will bind to it and keep it alive.
The code below creates five instances of the "value" variable, each with a distinct value and tied to a different thread.
Closures are a favorite for a lot of people to use because of how deceptively easy they are and how their interaction with anonymous lambda functions makes anonymous function usage and behavior intuitive.