Lambda Expressions — Inline Anonymous Functions

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
Note: Lambda expressions do not exist at runtime as a distinct construct — they are compiled to either (a) a regular method (if they do not capture variables) or (b) a class with fields for captured variables and an instance method (if they do capture). The compiler generates these automatically. Understanding this compilation model is important for performance: lambdas that capture variables allocate a closure object on the heap each time they are created. Lambdas passed to frequently-called methods in hot paths should avoid unnecessary captures.
Tip: Use expression lambdas (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.
Warning: The classic loop variable capture bug: in a 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.

🧠 Test Yourself

A lambda captures a local variable int maxRetries = 3. The lambda is passed to a retry helper and called later. Is the lambda using the value of maxRetries at capture time or at invocation time?