The scope of C# functional programming
In C#, the scope of variables is strictly determined. Its essence is that all codes live in the methods of the class, and all variables live in the modules that declare them, or in the code that follows. The value of a variable is variable. The more public a variable is, the more serious the problems it brings. The general principle is that the value of a variable is best kept unchanged, or its value is saved within the smallest scope. A pure function is best to use only variable values defined in its own module and not access any variables outside its scope.
Unfortunately, sometimes we cannot limit the value of a variable to the scope of a function. What if several variables are defined during the initialization of the program and they need to be used repeatedly later? One possible way is to use closures.
Closure mechanism for functional programming in C#
To understand the nature of closures, we analyze several examples of using closures:
namespace Closures
{
class Closures
{
static void Closures()
{
(GetClosureFunc()(30));
}
static Func<int,int> GetClosureFunc()
{
int val = 10;
Func<int, int> internalAdd = x => x + val;
(internalAdd(10));
val = 30;
(internalAdd(10));
return internalAdd;
}
}
}
What is the result output of this code? The answer is 20 40 60. You should be able to see the first two values, but why is the third value 60? Let’s take a look at the execution process of the program: the Closures function calls the GetClosureFunc function and enters it. The function call statement contains a parameter 30. This is because GetClosureFunc returns a function, that is, the function is called again during execution, and enters the GetClosureFunc function. First, the value of val is 10, and a value of 10 is passed through the internalAdd method. Therefore, the first output value is 20. Go down, the value of val becomes 30, and the value of 10 is passed through the internalAdd method, so the second output value is 40. From here we can roughly see how local functions and local variables work in the same scope. Obviously, changes to local variables will affect the value of internalAdd, although the change of variables occurs after the initial creation of internalAdd. Finally, GetClosureFunc returns the internalAdd method, and calls this function again with parameter 30, so the result becomes 60.
At first glance, this is not really logical. val should be a local variable, it lives in the stack, and when the GetClosureFunc function returns, it is gone, right? It is true, and this is exactly the purpose of the closure, and when the compiler will clearly warn that this situation will cause a program to crash, preventing the variable value from being out of scope.
From a technical point of view, where the data is saved is important. The compiler creates an anonymous class and creates an instance of this class in GetClosureFunc - if the closure is not required to work, that anonymous function will only live in the same class as GetClosureFunc. Finally, the local variable val is no longer a local variable, but a field in the anonymous class. As a result, internalAdd can now refer to functions stored in anonymous class instances. This example also contains data of the variable val. As long as the internalAdd reference is maintained, the value of the variable val will be kept.
The following code illustrates the pattern adopted by the compiler in this case:
private sealed class DisplayClass
{
public int val;
public int AnonymousFunc(int x)
{
return x + ;
}
private static Func<int, int> GetClosureFunc()
{
DisplayClass displayClass = new DisplayClass();
= 10;
Func<int, int> internalAdd = ;
(internalAdd(10));
= 30;
(internalAdd(10));
return internalAdd;
}
}
Go back to the idea of creating functions on the fly: new functions can now be created out of thin air, and their functions vary by parameter. For example, the following function adds a static value to a parameter:
private static void DynamicAdd()
{
var add5 = GetAddX(5);
var add10 = GetAddX(10);
(add5(10));
(add10(10));
}
private static Func<int,int> GetAddX(int staticVal)
{
return x => staticVal + x;
}
This principle is the basis of many function construction techniques, which obviously correspond to object-oriented methods such as method overloading. But unlike method overloading, the creation of anonymous functions can occur dynamically at runtime and only needs to be triggered by a line of code in another function. Special functions used to make an algorithm easier to read and write can be created in the method that calls it, rather than randomly adding functions or methods at the reclassification level - this is the core idea of function modularity.
Summarize
Closures are an important tool for programming languages to support functional design methods.