Yesterday I learned that C# 9 has static
anonymous functions and that I should probably use them by default, and you should probably too. But there's a caveat, you can only capture const
or static
state within them.
Here is an example:
int nonStaticVar = 1;
DoSomething(x => x + nonStaticVar);
The above lambda will capture nonStaticVar
from its calling environment, thus incurring at least one allocation in this case.
Because C# by default passes everything by value, the nonStaticVar
in the lambda is a value-copy of nonStaticVar
from the enclosing state. Even if nonStaticVar
was a reference type and not a value type like int
, a new reference would still have been created as a copy of the captured reference.
But this is not entirely all that C# does here. In fact, C# lambdas work a bit differently: C# actually creates a class behind the scenes that captures the enclosing state that the lambda accesses! This is called a closure.
What this means is that a class needs to be instantiated before executing the lambda or anonymous function, which we know means a heap allocation. Besides the overhead of the closure, C# also has the overhead of allocating a delegate for the anonymous function (basically a reference, or pointer in C-language, to that function along with its signature).
Introducing static anonymous functions
Static anonymous functions or methods were introduced in C# 9 to improve upon the state described above (pun intended), by removing the need for some allocations.
When you apply the static
modifier to a lambda or anonymous function, it can only reference static or constant objects from the enclosing state!
Even if you use a non-static anonymous function inside a static one, it won't be able to capture anything, except the variables declared in the enclosing static anonymous function's scope, but nothing from another level up — which would have been possible if the two anonymous functions were both not static!
Here is an example:
const int constVar = 1;
DoSomething(static x => x + constVar);
Here, the lambda can capture constVar
because it's a constant (constants are also static by definition).
But, something like the following wouldn't compile:
int nonStaticVar = 1;
DoSomething(static x => x + nonStaticVar); // <-- error!
This is because a static lambda is trying to capture a non-static variable. You will get an error along the lines of: error CS8821: A static anonymous function cannot contain a reference to 'this' or 'base'
, or similar.
What is the benefit?
How does this improve performance? Well, here is the feature specification where you can read about the motivation behind the feature, but basically by disallowing the capture of locals or instance state from the containing scope, one can prevent unexpected captures and unexpected additional allocations.
For instance, if your lambda captures an enclosing local scope variable, then C# will have to create a delegate and a closure that captures that variable, so 2 heap allocations.
If your lambda instead only captures an instance variable from the enclosing scope, then C# only creates a delegate because the enclosing type is already an object on the heap, so 1 extra heap allocation only.
With static anonymous functions, you would incur 0 heap allocations, because your lambda is not allowed to access non-static or non-const objects (which are allocated statically once)!
Take away
So, when possible, to avoid unnecessary and wasteful memory allocation, consider using static anonymous functions. I would actually go as far as writing all my lambdas with the static
modifier first, to avoid unintentional capturing and only when Intellisense or the compiler warns me, would I remove it.
If you need to capture non-static state though, consider if you can make that state static or const first.
End note
I hope this wasn't too long or too boring to read. I want to keep my future posts shorter if possible. If it was, please tell me in the comments. Thanks for reading!