A lambda expression is an anonymous function — a function defined inline without a name. The syntax x => x.Length creates a delegate that accepts x and returns x.Length. Lambda expressions are used in every LINQ query, every DI registration, every middleware configuration, and every async callback in modern C# code. They are not a separate feature — they compile to delegate instances (or expression trees, covered in Lesson 5). Understanding closures — how lambdas capture surrounding variables — is critical for avoiding subtle bugs.
Lambda Syntax
// ── Expression lambda — single expression, no return keyword ──────────────
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Func<string, bool> isEmail = s => s.Contains('@');
Action<string> print = s => Console.WriteLine(s);
Func<int> getForty = () => 42; // no parameters
// ── Statement lambda — block body with explicit return ─────────────────────
Func<int, string> describe = n =>
{
if (n < 0) return "negative";
if (n == 0) return "zero";
return "positive";
};
// ── Type inference — compiler infers parameter types from delegate type ────
var posts = new List<Post>();
var published = posts.Where(p => p.IsPublished); // p inferred as Post
var titles = posts.Select(p => p.Title); // return inferred as string
var top = posts.OrderByDescending(p => p.ViewCount).Take(5);
// ── Discard parameter (C# 9+) — when parameter is unused ──────────────────
Timer timer = new(_ => DoWork(), null, 0, 1000); // _ = unused state parameter
x => expression) whenever possible — they are more readable than statement lambdas and work with both delegate types and expression trees. Use statement lambdas (x => { ... }) only when you need multiple statements. For complex logic, consider extracting the lambda body into a named private method and passing a method group instead — named methods are easier to test, debug (stack traces show the method name), and reuse.for loop, all lambdas capture a reference to the same loop variable, not a copy of its value. When the lambdas execute after the loop, they all see the loop variable’s final value. Fix: copy to a local variable inside the loop before capturing. In foreach loops, C# 5+ guarantees each iteration gets its own variable (the bug was fixed in the language spec), so foreach is safe. For for loops, always copy: int captured = i; actions.Add(() => Console.WriteLine(captured));Closures — Capturing Variables
// ── Variable capture — lambda reads and writes the captured variable ──────
int count = 0;
Action increment = () => count++; // captures reference to count
increment();
increment();
Console.WriteLine(count); // 2 — lambda modified the outer variable
// ── Closure in a loop — the classic bug ──────────────────────────────────
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i)); // captures the variable i (not its value!)
}
actions.ForEach(a => a()); // prints: 3 3 3 (i is 3 after the loop)
// ── Fix — copy to local variable before capturing ─────────────────────────
var fixedActions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i; // new variable per iteration
fixedActions.Add(() => Console.WriteLine(copy)); // captures copy, not i
}
fixedActions.ForEach(a => a()); // prints: 0 1 2
// ── Practical closure — building a validator with captured configuration ───
int maxLength = 100;
Func<string, bool> validateTitle =
title => !string.IsNullOrWhiteSpace(title) && title.Length <= maxLength;
// maxLength is captured and read each time validateTitle is invoked
Common Mistakes
Mistake 1 — Loop variable capture in for loops
❌ Wrong — all lambdas print the same final value:
for (int i = 0; i < 3; i++)
tasks.Add(Task.Run(() => Console.WriteLine(i))); // all print 3!
✅ Correct — capture a copy:
for (int i = 0; i < 3; i++)
{ int n = i; tasks.Add(Task.Run(() => Console.WriteLine(n))); } // 0, 1, 2
Mistake 2 — Heavy allocations in hot-path lambdas
❌ Wrong — allocates a closure on every call inside a tight loop:
✅ Correct — use static lambdas (static x => x.Id) when there is no capture, or cache the delegate instance.