Menu

C# Lambda Expressions: Syntax, Func and Action, Closures and LINQ

How lambda expressions work in C#: the => syntax in all its forms, storing lambdas in Func and Action, passing them to methods and LINQ, statement lambdas, closures over captured variables, and the for loop capture bug that foreach does not have.

This page includes runnable editors - edit, run, and see output instantly.

A lambda expression is a small anonymous function written inline with the => operator ("goes to"). The parameters go on the left, the result on the right:

Output:

60.0
12
ORDER SHIPPED!
96.0, 180.0

A lambda has no type of its own. It becomes a delegate (an object that points to a function) of whatever delegate type the context expects: Func<decimal, decimal> here, a Predicate<T> for List.FindAll, an EventHandler for an event. Func delegates return a value and Action delegates do not; the Func and Action page lists the full family.

Syntax Forms

x => x * x                         // one parameter: parentheses optional
(x, y) => x + y                    // several parameters
() => DateTime.Now.Year            // no parameters
(int x, string s) => s.Length > x  // explicit parameter types
order =>                           // statement lambda: a block with return
{
    decimal subtotal = order.Qty * order.Price;
    return subtotal > 100m ? subtotal * 0.9m : subtotal;
}

An expression lambda has a single expression after =>, and its value is the result. A statement lambda has a block in braces, which can contain any statements and must return a value on every path if the delegate returns one (otherwise CS1643, Not all code paths return a value in lambda expression).

Newer versions added more forms, all optional:

  • C# 9: discard parameters, so several unused parameters can all be _ ((_, _) => Save()), and static lambdas that are forbidden from capturing variables.
  • C# 10: a "natural type", so var square = (int x) => x * x; infers Func<int, int>. Before that, var f = x => x * 2; fails; even in C# 10 it still fails without a parameter type, with CS8917 The delegate type could not be inferred.
  • C# 12: default parameter values, (int x, int y = 1) => x + y.

Passing Lambdas to Methods

Any method that takes a delegate parameter accepts a lambda. That is how you pass behavior, not just data:

Output:

report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf

Filter does not know what "keep" means; the caller decides with a lambda. List.Sort works the same way: the lambda is a comparison that returns negative, zero or positive, and Sort calls it as often as it needs to.

Lambdas in LINQ

LINQ is the place most C# code meets lambdas. Every query operator takes one: Where a condition, Select a projection, OrderBy a key, Sum a value to add up:

Output:

Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45

The lambdas do not run where they are written. LINQ stores them and calls them when the query is enumerated by foreach, ToList() or Sum(), which is called deferred execution. The LINQ page covers the operators themselves.

Closures: Captured Variables

A lambda can use local variables and parameters of the method it is written in. It captures the variable, not its current value, and the variable lives on as long as the lambda does:

Output:

1
2
3
1
50.0

Each call to MakeCounter creates a new count, so the two counters are independent. The discount lambda read rate when it ran, not when it was written, so it applied 50%. The compiler implements this by moving captured variables into a hidden class that the method and the lambda share.

The Loop Capture Gotcha

Capturing a loop variable is where closures bite. A for loop has one variable for the entire loop, so every lambda created in it shares that variable:

Output:

3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach

All three lambdas in the first loop ran after the loop had finished, when i was 3. Copying i into a variable declared inside the body gives each lambda its own. foreach gets this right by itself: since C# 5, its iteration variable is a new variable on every pass. Before C# 5 it had the same bug, which is why older answers online still recommend the copy for foreach too.

The same trap applies to anything that runs later: event handlers, timers, Task.Run, and LINQ queries built in a loop and enumerated after it.

Lambdas, Anonymous Methods and Local Functions

Lambdas replaced the C# 2 anonymous method syntax, which still compiles:

Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; };   // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0;                          // lambda

For a helper used only inside one method, C# 7.0 also offers local functions, which have a name, can be recursive and do not allocate a delegate unless you convert them to one. Use a lambda when you are passing behavior to something (LINQ, Sort, an event); use a local function or a private method when you are calling it yourself.

One more distinction appears with LINQ providers such as Entity Framework: a lambda assigned to Expression<Func<T, bool>> is not compiled to code at all but to a data structure describing the code, which the provider translates to SQL. The syntax is identical; the parameter type of the method you call decides which one you get.

Frequently Asked Questions

What is a lambda expression in C#?

An anonymous function written with the => operator: x => x * 2 takes x and returns x * 2. A lambda has no name of its own; it is converted to a delegate type such as Func<int, int> or Action<string> and can be stored in a variable, passed to a method or returned from one.

How do I write a lambda with multiple parameters or no parameters?

Put the parameters in parentheses: (a, b) => a + b for two, () => DateTime.Now for none. Only a single parameter may drop the parentheses (x => x + 1). You can also give the types explicitly: (int a, int b) => a + b.

What is a closure in a C# lambda?

A lambda that uses a local variable from the method around it captures that variable, not a snapshot of its value. The variable lives as long as the lambda does, and if either side changes it later, the other sees the change. This is what makes counters and callbacks work, and what causes the classic for loop capture bug.

Why do lambdas in a for loop all see the same value?

A for loop has one loop variable for the whole loop, so every lambda created inside captures the same i and sees its final value when it runs later. Copy it into a local inside the body (int copy = i;) and capture the copy. foreach does not have this problem since C# 5, because it creates a fresh variable per iteration.

Why does var f = x => x * 2 not compile?

The compiler cannot tell which delegate type you want or what type x is, so it reports CS8917 The delegate type could not be inferred (before C# 10, CS0815). Declare the type (Func<int, int> f = x => x * 2;) or, in C# 10 and later, type the parameter: var f = (int x) => x * 2;.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED