C# Lambda Expressions
A lambda expression is an unnamed piece of code that can be treated as a value. In C#, lambdas are most often used where a delegate is expected, such as a Func<T>, Action<T>, Predicate<T>, event handler, or LINQ query operation. They matter because they let you pass behavior into methods without writing a separate named method every time.
Lambdas are central to modern C# because they connect generics, delegates, and collections. Once you understand how the compiler turns a lambda into a delegate or expression tree, LINQ, callbacks, sorting, validation, and asynchronous workflows become much easier to read and design.
Overview / How It Works
A lambda expression has parameters on the left, the => lambda operator in the middle, and an expression or block of statements on the right. The lambda itself does not declare its complete type. Instead, the compiler uses the target type from the assignment, method argument, or return type.
For example, when C# sees Func<int, int> square = n => n * n;, it knows the lambda must accept one int and return one int. The parameter type can often be inferred, so you write n instead of int n. If inference is unclear, you can add explicit parameter types.
Most lambdas are converted to delegates. A delegate is an object that can refer to a method with a particular signature. The compiler creates a method-like implementation for the lambda and stores a reference to it in the delegate object. Calling the delegate later runs the lambda body.
Lambdas can also be converted to expression trees, usually Expression<Func<...>>. Expression trees represent code as data instead of executable instructions. LINQ providers such as Entity Framework can inspect that tree and translate it into SQL. Ordinary collection LINQ, such as List<T>.Where through Enumerable.Where, uses delegates and executes in memory.
A lambda may capture local variables from the surrounding method. Captured variables live in a compiler-generated object so the lambda can still access them after the original scope would normally have moved on. This behavior is called a closure. Closures are powerful, but they can surprise you because the lambda captures the variable, not merely the value it had when the lambda was created.
Syntax
Func<int, int> square = x => x * x;
Action<string> log = message => Console.WriteLine(message);
var activeItems = items.Where(item => item.IsActive);
| Part | Meaning |
|---|---|
x, message, item |
The lambda parameter list. Parentheses are optional for one inferred parameter. |
=> |
The lambda operator, read as goes to. |
x * x |
An expression body. Its value becomes the return value. |
{ ... } |
A statement body. Use this when you need multiple statements; return a value explicitly for non-void delegates. |
Func<...> |
A delegate type that returns a value. |
Action<...> |
A delegate type that returns void. |
Use parentheses for zero parameters, multiple parameters, explicit parameter types, or modifiers such as ref. Examples include () => DateTime.Now, (a, b) => a + b, and (int a, int b) => a + b.
Examples
Example 1: Store Small Operations in Delegates
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
Predicate<int> isEven = n => n % 2 == 0;
Func<int, int> square = n => n * n;
foreach (int number in numbers)
{
if (isEven(number))
{
Console.WriteLine($"{number} squared is {square(number)}");
}
}
}
}
Output:
2 squared is 4
4 squared is 16
This program stores two lambdas in delegate variables. Predicate<int> represents a method that accepts an int and returns bool. Func<int, int> represents a method that accepts an int and returns an int. The loop calls these delegates exactly as if they were named methods.
Example 2: Use Lambdas with LINQ
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
record Course(string Title, int SeatsLeft, double Rating);
static void Main()
{
List<Course> courses = new List<Course>
{
new Course("C# Basics", 0, 4.7),
new Course("LINQ in Practice", 5, 4.9),
new Course("Async C#", 2, 4.8)
};
var available = courses
.Where(course => course.SeatsLeft > 0)
.OrderByDescending(course => course.Rating)
.Select(course => $"{course.Title}: {course.SeatsLeft} seats, {course.Rating:0.0}");
foreach (string line in available)
{
Console.WriteLine(line);
}
}
}
Output:
LINQ in Practice: 5 seats, 4.9
Async C#: 2 seats, 4.8
Each LINQ method receives a different lambda. Where receives a predicate that decides whether an item remains. OrderByDescending receives a key selector. Select receives a projection that turns a Course into a formatted string. The query is lazily evaluated, so the lambdas run when the foreach asks for results.
Example 3: Captured Variables and Closures
using System;
class Program
{
static void Main()
{
int threshold = 10;
Func<int, bool> isLarge = value => value > threshold;
Console.WriteLine(isLarge(12));
threshold = 20;
Console.WriteLine(isLarge(12));
int calls = 0;
Func<string> nextMessage = () =>
{
calls++;
return $"Call {calls}";
};
Console.WriteLine(nextMessage());
Console.WriteLine(nextMessage());
}
}
Output:
True
False
Call 1
Call 2
The lambda isLarge captures the variable threshold. After threshold changes from 10 to 20, the same delegate observes the new value. The second lambda captures and mutates calls, so it remembers state between invocations.
How It Works Step by Step / Under the Hood
- The compiler reads the target delegate type, such as
Func<int, bool>. - It checks that the lambda parameter list and body match that delegate signature.
- For a non-capturing lambda, the compiler can emit a static helper method and reuse a delegate instance.
- For a capturing lambda, the compiler creates a hidden closure class with fields for captured variables.
- The delegate points at the generated method. When the delegate is invoked, the CLR calls that method.
- If the target type is an expression tree, the compiler builds an object graph describing the lambda instead of normal executable delegate code.
The important runtime idea is that a lambda is not magic syntax that runs immediately. It becomes a value. That value can be stored, passed, returned, composed with other methods, or executed later.
Common Mistakes
Forgetting return in a Statement Lambda
var numbers = new[] { 1, 2, 3 };
var matches = numbers.Where(n => { n > 1; });
This does not compile. A statement-bodied lambda that returns a value must use return. Without braces, an expression-bodied lambda returns the expression automatically.
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3 };
var matches = numbers.Where(n => { return n > 1; });
foreach (int number in matches)
{
Console.WriteLine(number);
}
}
}
Output:
2
3
Capturing a for Loop Variable by Accident
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.Write(i));
}
foreach (Action action in actions)
{
action();
}
}
}
Output:
333
All three lambdas capture the same i variable from the for loop. By the time the actions execute, the loop has ended and i is 3. Create a local copy inside the loop when each lambda should keep its own value.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.Write(copy));
}
foreach (Action action in actions)
{
action();
}
}
}
Output:
012
Best Practices
- Use expression-bodied lambdas for short, obvious transformations and predicates.
- Use statement-bodied lambdas only when multiple statements improve clarity.
- Prefer named methods when the logic is long, reused, recursive, or needs independent tests.
- Be careful when capturing variables that later change, especially in loops or asynchronous code.
- Keep LINQ lambdas free of surprising side effects; filtering and projection code should usually be pure.
- Use explicit parameter types when overload resolution or reader understanding would benefit.
- Remember that query providers may translate expression-tree lambdas, so not every .NET method can be used inside database queries.
Practice Exercises
- Create a
Func<decimal, decimal>lambda that applies an 8 percent discount and prints the discounted price of 50. - Given a list of names, use
WhereandSelectwith lambdas to print uppercase names that have at least five characters. - Write a method that accepts an
Action<string>logger, then call it once with a lambda that writes messages to the console.
Summary
- A lambda expression is inline behavior that is converted to a delegate or expression tree.
Funcreturns a value,Actionreturnsvoid, andPredicate<T>returnsbool.- Expression-bodied lambdas return their expression automatically; statement-bodied lambdas need
returnwhen returning a value. - Lambdas can capture surrounding variables through closures, which preserve variables, not just old values.
- LINQ uses lambdas heavily for filtering, sorting, grouping, and projecting data.
