C# Lambda Default Parameters: Syntax and Limits
Learn how to use default values in C# lambda expressions: C# 12 syntax, compile-time rules, call-site behavior, and workarounds for older versions.
C# 12 lets you specify default values for lambda parameters. This article covers the syntax, behavior, and limitations of lambda default parameters, along with practical alternatives for projects on older language versions.
C# 12 Syntax for Lambda Default Parameters
A lambda expression can specify a default value for any of its parameters by placing the value after the parameter name and an equals sign:
var add = (int a, int b = 10) => a + b;
The compiler treats b as an optional parameter. Calling add(5) produces the same result as add(5, 10). The default value must be a compile-time constant, exactly as with method parameters. You cannot use a variable, a property, or a method call as the default.
Parameter order matters: any parameter with a default value must appear to the right of parameters that do not have defaults. The following declaration is invalid:
var invalid = (int a = 1, int b) => a + b; // CS1763: Optional parameters must appear after all required ones
The rule matches the behavior of optional parameters in ordinary methods and prevents ambiguous calls.
How Default Values Behave at the Call Site
When you invoke a lambda that has default parameters, you can omit the trailing arguments. The compiler substitutes the default values at the call site, not at runtime. This means the default value is baked into the generated code, and changing the default value requires recompiling all callers that rely on it.
var greet = (string name, string greeting = "Hello") => $"{greeting}, {name}!"; Console.WriteLine(greet("Alice")); // Hello, Alice! Console.WriteLine(greet("Bob", "Hi")); // Hi, Bob!
You can also use named arguments to skip a parameter that has a default, though this is less common with lambdas:
var format = (int value, string prefix = "Value: ", bool uppercase = false) => uppercase ? prefix.ToUpper() + value : prefix + value; Console.WriteLine(format(42, uppercase: true)); // VALUE: 42
Named arguments work because the compiler knows the parameter names from the lambda's signature.
Optional parameters also follow normal positional-argument rules. If several trailing parameters have defaults, omitted arguments are filled from left to right. To pass a value for a later parameter while keeping the default for an earlier one, use a named argument.
Compile-Time Constraints and Error Cases
Default values in lambda parameters must be compile-time constants. A literal such as null is allowed because null is a constant, but a runtime value such as Environment.TickCount or DateTime.Now is not. The compiler reports error CS1736 when the default is not a constant.
The feature is only available in C# 12 and later. If you compile with an older language version, you get a compiler error. The runtime does not need to support the feature; it is purely a compiler feature, and the generated code uses the default values directly.
Workarounds for C# Versions Before 12
If your project targets an older C# version, you have two practical options for achieving similar behavior.
The first is to use a delegate type that declares default parameter values. The lambda assigned to the delegate must match the delegate's signature, but the defaults are defined on the delegate, not on the lambda itself:
public delegate int Operation(int a, int b = 10); Operation add = (a, b) => a + b; Console.WriteLine(add(5)); // 15
The lambda itself does not declare defaults; the delegate type supplies them. This works because the compiler applies the delegate's default values when the delegate is invoked. The limitation is that the defaults are fixed by the delegate type, and you cannot have different lambdas with different defaults for the same delegate type.
The second approach is to use nullable parameters and check for null inside the lambda:
Func<int, int?, int> add = (a, b) => a + (b ?? 10);
This gives you runtime flexibility, but the caller must still pass null explicitly unless you also define a separate overload. It does not provide the same call-site convenience as true optional parameters.
Practical Use Cases for Lambda Default Parameters
Lambda default parameters are most useful when you define inline callbacks or configuration functions where a common fallback value is natural. For example, a logging callback with a default severity level:
var log = (string message, string? level = "INFO") => Console.WriteLine($"[{level}] {message}");
They also simplify test helpers and small factory functions where you want to avoid overloads. Because the default is part of the lambda's signature, you can store the lambda in a variable and call it consistently.
One area to be careful with is expression trees. A lambda expression that declares default parameter values cannot be converted to Expression<TDelegate>; the expression tree syntax cannot represent those defaults, and the compiler rejects it with error CS0854. If you need expression-tree compatibility, you can put the defaults on a custom delegate type instead, as long as the lambda itself declares no defaults. The nullable-parameter pattern also works because the fallback is in the lambda body.
Compatibility and Maintainability Considerations
Using lambda default parameters requires the code to be compiled with C# 12 or later. If you distribute source or generate C# code (for example, through a source generator or template), consumers using an older language version cannot compile that code. If you ship a compiled assembly, consumers do not need C# 12 to use it; only the library project itself must be built with C# 12.
From a maintainability perspective, default values on lambdas can hide important configuration. A reader may not immediately see that a callback has optional behavior. It is often clearer to use a separate overload or a delegate with defaults, because the defaults are visible at the type level. For small, local lambdas, the convenience usually outweighs the readability cost.
Choosing Between Lambda Defaults and Delegate Defaults
The decision comes down to where you want the default to live. If the default belongs to a specific lambda instance, use C# 12 lambda default parameters. If the default is a property of the delegate contract itself, define it on the delegate type. The table below summarizes the tradeoffs:
| Approach | Default location | C# version | Call-site flexibility | Expression tree support |
|---|---|---|---|---|
| Lambda default | Lambda signature | C# 12+ | Per lambda | No |
| Delegate default | Delegate type | C# 4+ | Fixed per delegate | Yes (via delegate type) |
| Nullable parameter | Lambda body | C# 3+ | Caller must pass null | Yes (with care) |
Use lambda defaults when you need different defaults for different lambdas that share the same delegate type. Use delegate defaults when the default is an invariant part of the callback contract or when you need expression-tree compatibility. Use nullable parameters when the default must be computed at runtime.