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.

To download the source code for this article, you can visit our GitHub repository.

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. 

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

If you want to learn more about benchmarking, feel free to check out our article Introduction to Benchmarking in C# and ASP.NET Core Projects.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!