In this article, we’ll review static anonymous functions, which were introduced in C# 9. To understand how they can improve our code, we should first have a look at non-static anonymous methods and lambda expressions. Let’s see what happens, as far as memory allocation is concerned, when one of the latter is called.
Let’s dive in.
Non-static Anonymous Function
Anonymous methods and lambda expressions are costly. Naturally, a single invocation doesn’t impair performance noticeably. However, there may be multiple invocations in our program, like for instance in a loop. If this is the case, these tiny performance losses start building up.
Let’s have a look at a single invocation and see what is going on when we call an anonymous method. There may be zero, one, or two heap allocations depending on what the method captures from the enclosing state. If it captures the enclosing instance state, there is just a delegate allocation. If it captures a local variable or argument, there are two heap allocations, one for the closure and one for the delegate. There are no heap allocations only if the method doesn’t capture anything or captures a static state.
Let’s see how an anonymous method captures a variable from the enclosing scope:
private double _numberInEnclosingScope = 4; void Calculate(Func<double, double> func) { Console.WriteLine(func(6)); } public void Display() { Calculate(num => Math.Pow(_numberInEnclosingScope, num)); }
The lambda expression captures the variable _numberInEnclosingScope
from the enclosing scope, which causes unintended memory allocation. We can fix it by turning the anonymous method into a static one.
Static Anonymous Function
To convert a non-static anonymous method or lambda expression into a static anonymous function, we have to use the static
modifier. Static anonymous functions do not capture variables from the enclosing scope, but they can still reference them if certain conditions are met.
What the Function Has Access To
We only have access from within a static anonymous function to variables defined in the enclosing scope if we mark them as const
or static
. If we don’t want the _numberInEnclosingScope
variable in our example to be captured, but we still want to use it inside the lambda expression, we should make it constant:
private const double _numberInEnclosingScope = 4; public void Display() { Calculate(static num => Math.Pow(_numberInEnclosingScope, num)); }
Just like before, we should see the result of raising one number to the power of the other. Naturally, we won’t notice any performance boost. But the code is indeed more performant than before. This is because the lambda expression now doesn’t capture the variable. Just take our word for it for now. We’re going to demonstrate how our choice of the static function over its non-static counterpart impacts memory allocation. For this, we will benchmark our functions later in the article.
Let’s now mark the variable as static
to see whether it’s still accessible:
private static double _numberInEnclosingScope = 4; public void Display() { Calculate(static num => Math.Pow(_numberInEnclosingScope, num)); }
It works just as fine and we don’t get any errors. So, we just proved that we can access variables from the enclosing scope if certain conditions are met. However, there are some limitations as to what other variables we can reference from within a static anonymous function.
What the Function Doesn’t Have Access To
There are some variables a static anonymous function doesn’t have access to. It doesn’t have access to variables defined in the same class that we normally reference with the this
keyword. This group also contains variables defined in the base class that we reference using the base
keyword. Finally, it can’t reference locals and parameters. To demonstrate it, let’s create two classes DemoStaticBase
and its derived class DemoStaticDerivative
:
public class DemoStaticBase { public double numberInBase = 3; } public class DemoStaticDerivative : DemoStaticBase { private double _numberInThis = 4; void Calculate(Func<double, double> func) { Console.WriteLine(func(6)); } public void Display(double numberInParameter) { double numberInLocal = 2; // Error CS8821 Calculate(static num => Math.Pow(this._numberInThis, num)); } }
Here we can see all four of the aforementioned types of variables, with pretty self-explanatory names: numberInBase
, _numberInThis
, numberInParameter
and numberInLocal
. We’re trying to use this
in the static anonymous function, but Visual Studio immediately shows us an error:
Error CS8821: A static anonymous function cannot contain a reference to 'this' or 'base'.
By the way, we also get the message that the name can be simplified. This is because the this
keyword in the lambda expression is redundant. We used it just to emphasize its existence there.
Next, if we try to use the numberInBase
variable defined in the base class, this isn’t going to work. We received the same error during the compile time.
If we try to use numberInLocal
or numberInParameter
in the lambda expression:
Calculate(static num => Math.Pow(numberInLocal, num));
We receive a slightly different error:
Error CS8820: A static anonymous function cannot contain a reference to 'numberInLocal'.
In our examples, the static anonymous function was pretty simple, but it doesn’t have to be the case. For example, nothing stops us from defining local variables and methods in it. The question is, how do they behave? Do local methods have access to the state in the enclosing static anonymous function? Let’s see this in action.
Non-static Local Methods
Let’s define a non-static local function and see whether it can capture the state from the enclosing static function:
public void Display() { Calculate(static num => { double numberInStatic = 5; double AddNumbers() { return num + numberInStatic; } return Math.Pow(AddNumbers(), 2); }); }
Here, we define a non-static local method inside our static anonymous function. It turns out we have access to the num
parameter and the local numberInStatic
variable defined in the enclosing function. However, we still can’t reference any other variables that are not accessible to the static anonymous function itself.
This all looks just great, but we still haven’t seen any performance gain. Let’s benchmark our code to see whether there is any.
Performance Gain
As static anonymous functions are all about performance, we’ll actually benchmark the performance using the BenchmarkDotNet
library.
First, let’s create three methods that will be benchmarked:
private int _numNonConst = 10; private const int _numConst = 10; public int Calculate(Func<int, int> func) { return func(6); } [Benchmark] public int MultiplyNonStatic() { return Calculate(num => _numNonConst * num); } [Benchmark] public int MultiplyNonStaticWithConst() { return Calculate(num => _numConst * num); } [Benchmark] public int MultiplyStatic() { return Calculate(static num => _numConst * num); }
These are very simple methods that just multiply two integer numbers and return the result. The first one uses a non-static lambda expression with a non-constant variable. The second one uses a non-static lambda expression with a constant variable. The third method uses a static lambda expression with a constant variable.
Let’s have a look at the results:
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated | |--------------------------- |---------:|----------:|----------:|---------:|-------:|----------:| | MultiplyNonStaticWithConst | 1.302 ns | 0.0162 ns | 0.0144 ns | 1.299 ns | - | - | | MultiplyStatic | 1.551 ns | 0.0160 ns | 0.0125 ns | 1.555 ns | - | - | | MultiplyNonStatic | 6.552 ns | 0.1619 ns | 0.3157 ns | 6.401 ns | 0.0153 | 64 B |
So, we can clearly see that two of the methods are faster and don’t allocate any memory. These are the methods with the static anonymous function and the method with the non-static anonymous function with a constant variable. It seems that static anonymous functions owe their performance gain to the very nature of constants and statics in C#.
Conclusion
Static anonymous functions can’t capture the state from the enclosing scope and can reference constant and static variables only. Thus they can improve performance by reducing unexpected memory allocations. We benchmarked them to see whether there is any performance gain and it turned out that they are indeed faster and more performant. Using them makes the most sense if there are numerous calls to anonymous methods or lambda expressions. They are definitely a useful feature of C# that we can leverage.