C# Lambda Expressions, Re-run on .NET 10
C# lambdas on .NET 10: natural types, C# 12 default parameters, static lambdas, the loop-capture trap, and measured capture cost.
Revised November 2026. This tutorial first appeared in Korean in 2021. We recompiled its examples on .NET 10.0.14 (C# 14). The first example did not compile because two delegates shared a name (error CS0102), the statement-lambda snippet was missing a semicolon (error CS1002), and the original title misspelled "Lambda" as "Lamda". Below are the fixed examples, the lambda features C# has gained since 2021, and a measurement of what capturing actually costs.
If you learned lambdas in the C# 7–9 era, or you are wondering why var f = (a, b) => a + b; refuses to compile, this article is for you. Short version: a lambda is an unnamed method written as an expression, and the compiler infers its parameter types from the delegate type it is assigned to. When there is no such type you must spell the types out, and when a lambda captures an outer variable it allocates. Those two facts explain most lambda surprises in real code.
A lambda is an anonymous method, shortened
A method is parameters, a body and a return value. A lambda keeps only those and drops everything else. Compared with the older anonymous-method syntax (delegate (…) { … }), it is much shorter. The diagram below is from the original article; the comments read "anonymous method", "lambda" and "equivalent lambda".

You can omit the types of a and b because the compiler reads them from the signature of MyDelegate on the left. All four lines below compile on .NET 10 and give the same result:
delegate int MyDelegate(int a, int b);
MyDelegate A = delegate (int a, int b) { return a + b; }; // anonymous method
MyDelegate B = (int a, int b) => a + b; // lambda with types
MyDelegate C = (a, b) => a + b; // lambda, types inferred
MyDelegate D = delegate { return 42; }; // anonymous method may drop the parameter list
Console.WriteLine($"{A(1,2)} {B(1,2)} {C(1,2)} {D(1,2)}");
3 3 3 42
Only anonymous methods can drop the parameter list entirely, as in delegate { … }. You still see that form in event handlers that ignore their arguments. With lambdas you use discards instead: (_, _) => 0 (C# 9).
Compiling the 2021 example on .NET 10
The original first example failed as written:
class MainApp {
delegate int MyDelegate(int a, int b);
delegate void MyDelegate(); // <- same name
static void Main(string[] args) {
MyDelegate add = (a, b) => a + b;
MyDelegate2 lamda = () => Console.WriteLine("lambda");
...
Program.cs(3,19): error CS0102: The type 'MainApp' already contains a definition for 'MyDelegate'
The body uses MyDelegate2, so the declaration was the typo. C# cannot overload delegate types by parameter count (only by the number of generic type parameters, which is how Func<T> and Func<T1,TResult> coexist). The original also had no using System;. That passes in any .NET 6+ project because ImplicitUsings is on by default; switch it off and you get:
error CS0103: The name 'Console' does not exist in the current context
Fixed version and its output:
using System;
class MainApp
{
delegate int MyDelegate(int a, int b);
delegate void MyDelegate2();
static void Main(string[] args)
{
MyDelegate add = (a, b) => a + b;
MyDelegate2 lambda = () => Console.WriteLine("lambda");
Console.WriteLine("11 + 22 = {0}", add(11, 22));
lambda();
}
}
11 + 22 = 33
lambda
Statement lambdas
When one expression is not enough, wrap the body in braces to get a statement lambda. The 2021 snippet left out the semicolon after the closing brace. The lambda is part of a variable declaration, so the statement must end with };:
Program.cs(12,2): error CS1002: ; expected
Here is the fixed version, with calls for all three branches:
delegate void MyDelegate(int a, int b);
MyDelegate Compare = (a, b) =>
{
if (a > b)
Console.WriteLine("{0} is larger", a);
else if (a < b)
Console.WriteLine("{0} is larger", b);
else
Console.WriteLine("{0} and {1} are equal", a, b);
}; // <- the semicolon missing from the 2021 snippet
Compare(11, 22);
Compare(22, 11);
Compare(7, 7);
22 is larger
22 is larger
7 and 7 are equal
Since C# 10: lambdas with var (natural type)
In 2021 (C# 9) a lambda always had to be assigned to a delegate-typed variable. C# 10 gave lambdas a natural type, so var works. We printed GetType() to see what the compiler picks:
var add = (int a, int b) => a + b;
var hello = () => Console.WriteLine("hi");
var pick = object (bool b) => b ? 1 : "one"; // explicit return type (C# 10)
Console.WriteLine(add.GetType());
Console.WriteLine(hello.GetType());
Console.WriteLine(pick.GetType());
System.Func`3[System.Int32,System.Int32,System.Int32]
System.Action
System.Func`2[System.Boolean,System.Object]
Func and Action are the defaults. Leave out the parameter types, though, and the compiler has nothing to infer from. The same error appears for a method group with several overloads, such as int.Parse:
var f = (a, b) => a + b; // no types
var parse = int.Parse; // overloaded
error CS8917: The delegate type could not be inferred.
When branches return different types, as in pick (int vs string), put the return type in front of the lambda.
C# 12: default parameters and params in lambdas
Since C# 12 lambda parameters can have default values and params. The natural type then becomes a compiler-generated anonymous delegate type rather than Func, because Func cannot carry default values:
var inc = (int x, int step = 1) => x + step;
Console.WriteLine(inc(10) + " " + inc(10, 5));
Console.WriteLine(inc.GetType());
var sum = (params int[] xs) => xs.Sum();
Console.WriteLine(sum(1, 2, 3));
11 15
<>f__AnonymousDelegate0`3[System.Int32,System.Int32,System.Int32]
6
The trap is assigning such a lambda to Func. It compiles with a warning, and the default is silently gone: you can no longer call h(1).
Func<int, int, int> h = (int x, int step = 1) => x + step;
warning CS9099: Parameter 2 has default value '1' in lambda but '<missing>' in the target delegate type.
Lambdas with out parameters also work with var and likewise get an anonymous delegate type, because Func and Action do not support ref/out. Our Func and Action delegates article covers that limit in detail.
Static lambdas and what capture costs (measured)
When a lambda uses an outer local variable, the compiler generates a closure object that holds the variable and turns the lambda into a method on that object. Marking a lambda static (C# 9) makes any capture a compile error:
int offset = 10;
Func<int, int> g = static x => x + offset;
error CS8820: A static anonymous function cannot contain a reference to 'offset'.
To see what capture costs, we called a method that takes a Func<int,int> 10 million times in three ways: (1) passing a non-capturing lambda written inside the loop, (2) passing a lambda that captures a loop-local variable, and (3) creating the capturing lambda once, outside the loop.
[MethodImpl(MethodImplOptions.NoInlining)]
static int Apply(Func<int, int> f, int x) => f(x);
// (1) no capture
for (int i = 0; i < n; i++) s += Apply(x => x * 2, i);
// (2) capture inside the loop
for (int i = 0; i < n; i++) { int k = i & 7; s += Apply(x => x * k, i); }
// (3) capture, created once
int k = 3; Func<int, int> f = x => x * k;
for (int i = 0; i < n; i++) s += Apply(f, i);
// allocation: GC.GetAllocatedBytesForCurrentThread() before/after
.NET 10.0.14, Release build, Apple M1 Pro, 10 million iterations, three runs:
| Case | Run 1 | Run 2 | Run 3 | Allocated |
|---|---|---|---|---|
| (1) no capture | 19.1 ms | 19.4 ms | 19.3 ms | 0 B |
| (2) capture inside loop | 136.5 ms | 137.8 ms | 133.8 ms | 839.2 MB (88 B/iter) |
| (3) capture, created once | 19.3 ms | 19.0 ms | 19.1 ms | 0 B |
A non-capturing lambda is cached by the compiler in a static field, so writing it inside the loop allocated nothing. Case (2) builds a new closure and a new delegate every iteration: 88 bytes each and roughly 7x the time. A tenth of a second per 10 million calls is irrelevant in most code, but in a game loop or a server path handling tens of thousands of requests per second, hoist the lambda out of the loop as in (3), or mark it static so the compiler stops you from capturing. LINQ calls such as Where(x => x > limit) capture limit and follow the same rule.
The loop-capture trap
A lambda captures the variable itself, not its current value. Lambdas created in a for loop and run later therefore surprise people:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
actions.Add(() => Console.Write(i + " "));
foreach (var a in actions) a(); // for
actions.Clear();
foreach (var i in new[] { 0, 1, 2 })
actions.Add(() => Console.Write(i + " "));
foreach (var a in actions) a(); // foreach
actions.Clear();
for (int i = 0; i < 3; i++)
{
int copy = i; // a new variable per iteration
actions.Add(() => Console.Write(copy + " "));
}
foreach (var a in actions) a(); // for + copy
3 3 3 <- for
0 1 2 <- foreach
0 1 2 <- for + copy
The for loop has one i for its whole lifetime, so all three lambdas see its final value, 3. Since C# 5, foreach creates a fresh iteration variable each time, giving 0 1 2. For the same reason, a captured variable changed inside a lambda is changed outside too:
int counter = 0;
Action incr = () => counter++;
incr(); incr();
Console.WriteLine($"counter = {counter}"); // counter = 2
You hit this most often with loop variables used inside async callbacks or Task.Run.
Which form to use when
| Situation | Use |
|---|---|
| One-line calculation, LINQ predicate | expression lambda x => x * 2 |
| Several statements, branching | statement lambda x => { …; } (do not forget the final ;) |
| Keep it in a local and reuse | var f = (int x) => … (C# 10+, types required) |
| Branches return different types | explicit return type: object (bool b) => … |
| Callback with an optional argument | lambda default parameter (C# 12); assigning to Func drops it |
| Called in a hot loop | avoid capture, use a static lambda, or create it outside the loop |
| Parameters unused | (_, _) => … or anonymous method delegate { … } |
The delegate type you will store lambdas in is usually Func or Action; read Func and Action delegates next, and LINQ basics for where lambdas are used most.
Tested with .NET SDK 10.0.403 / runtime 10.0.14, C# 14, macOS on Apple M1 Pro.
FAQ
When can I leave out the parameter types of a lambda?
When the target delegate type is known. MyDelegate add = (a, b) => a + b; compiles, but var f = (a, b) => a + b; fails with error CS8917 (The delegate type could not be inferred). With var, write the types: (int a, int b) => a + b.
Why do lambdas created in a for loop all print the same value?
A lambda captures the variable, not its value. The i of a for loop is a single variable for the whole loop, so all three lambdas print its final value 3. foreach creates a fresh variable per iteration (since C# 5) and prints 0 1 2. In a for loop, copy it first: int copy = i;
Do lambdas allocate memory?
A lambda that captures nothing is cached and allocated 0 bytes across 10 million calls in our test. A lambda that captures a loop-local variable allocated 88 bytes per creation (closure plus delegate): 839 MB over 10 million iterations, and about 7x the time (19 ms vs 136 ms) on .NET 10, Release, Apple M1 Pro.