C# Static Lambda: Syntax, Benefits, and Usage
Learn how C# static lambdas avoid capturing instance state, reduce allocations, and improve performance in delegate-heavy code.
In C# 9, static lambda expressions (also known as static anonymous functions) allow you to define a lambda that cannot capture state from the enclosing method or class. This restriction is enforced by the compiler, which prevents accidental captures and can reduce memory allocations in performance-sensitive code. The feature is particularly useful when you are passing delegates to high-frequency callbacks or using lambdas in hot paths where avoiding extra object creation matters.
Understanding Static Lambda Syntax
To declare a static lambda, add the static keyword before the parameter list. The syntax mirrors a regular lambda but signals that the lambda body cannot reference any instance members, local variables, or parameters from the enclosing scope. Here is a minimal example:
Func<int, int> square = static x => x * x;
The compiler enforces that x is the only variable used inside the body. If you attempt to reference an outer variable, you get a compile-time error. This is the core difference from a regular lambda, which can capture variables from the enclosing scope.
How Static Lambdas Differ from Regular Lambdas
A regular lambda can capture variables from the enclosing method or class, which may cause the compiler to generate a closure class to hold those captured values. This closure is allocated on the heap, and the delegate points to a method on that closure instance. In contrast, a static lambda has no captures, so the compiler can cache a single delegate instance and reuse it across calls. This reduces memory traffic and can improve cache locality.
Consider this example:
int offset = 5; Func<int, int> addOffset = x => x + offset; // captures offset
The lambda addOffset captures offset, so each time this code executes, a new closure object may be created. If you convert it to a static lambda, you must pass offset as a parameter:
int offset = 5; int value = 10; Func<int, int, int> addOffsetWithParam = static (x, y) => x + y; int result = addOffsetWithParam(value, offset);
This achieves the same behavior without capture.
When to Use Static Lambdas
Use a static lambda whenever the lambda does not need to access state from the enclosing scope. This is common in utility methods, pure functions, and transformations where the lambda only depends on its arguments. For example, in LINQ queries that operate on a collection, you often write lambdas that only use the element being processed:
var squares = numbers.Select(static n => n * n);
Here, n is the only input, and no external state is required. Marking it static makes the intent clear and lets the compiler optimize delegate reuse.
Another scenario is when you are defining a delegate that is used repeatedly, such as a comparison or a predicate. A static lambda can be cached as a static field, avoiding repeated delegate creation:
private static readonly Func<int, bool> IsEven = static n => n % 2 == 0;
Because the lambda does not capture anything, the same delegate instance can be reused safely across threads and calls.
Performance and Allocation Benefits
The primary performance benefit of static lambdas is the elimination of closure allocations. When a regular lambda captures variables, the compiler generates a closure class and instantiates it each time the lambda is created. This allocation occurs even if the lambda is only used once, adding pressure on the garbage collector. Static lambdas avoid this entirely because there is no captured state to store.
In high-throughput code, such as processing millions of items in a loop, reducing allocations can have a measurable impact on latency and memory usage. The exact improvement depends on how often the lambda is created and how long it lives. For a one-off lambda, the difference may be negligible, but for a delegate that is created repeatedly, the savings add up.
It is important to note that static lambdas do not automatically make your code faster; they simply remove a potential source of allocation. If a lambda does not capture anything, the compiler can already cache the delegate in some cases; marking it static makes the no-capture intent explicit and ensures the compiler will reject accidental captures.
Common Mistakes and Limitations
One common mistake is trying to use a static lambda when you actually need to capture a value. The compiler will reject the code, so you must refactor to pass the value as a parameter. This can make the calling code slightly more verbose, but it often clarifies the data flow.
Another limitation is that static lambdas cannot access this or any instance members, even if the lambda is defined inside an instance method. This is by design, as the goal is to prevent implicit captures. If you need to access instance state, you must either use a regular lambda or extract the state into a local variable and pass it explicitly.
Static lambdas cannot capture ref, out, or in parameters from the enclosing method. This is not unique to static lambdas: regular lambdas and anonymous methods also cannot capture those parameter kinds. If you need to modify an outer variable with a ref or out argument, a lambda is not the right tool.
Static Lambdas with Expression Trees and Delegates
Static lambdas can be converted to both delegate types and expression trees. When converted to an expression tree, the static modifier does not change the expression tree shape; it only affects whether the compiler allows captures. For expression trees, captured values are represented in tree nodes rather than in a compiler-generated closure, so the static restriction may not provide additional memory benefits. However, it can make the code clearer if you intend to use the expression as a pure function.
For delegates, the static modifier can help the compiler avoid closure allocation. This is useful in generic code as well. For example:
static T Identity<T>(T value) { Func<T, T> identity = static (T x) => x; return identity(value); }
The static lambda does not depend on captured state, so no closure is needed. The compiler can cache the delegate for each constructed Func<T, T> type.
Compatibility and Compiler Support
The static modifier for anonymous functions was introduced in C# 9. To use it, your build tools must support C# 9 and the project's language version must allow it. For .NET Core 3.1 projects, you can enable C# 9 by setting the LangVersion to 9.0 in the project file. Most modern .NET projects use C# 9 or later by default, so this is rarely a blocker.
When working with older codebases, you can still achieve similar allocation benefits by manually caching a delegate in a static field. However, the static keyword provides compile-time enforcement and makes the intent self-documenting, which is valuable for maintainability.