Lambda Expressions
A lambda expression is a concise way to write an anonymous function inline, using the => ('goes to') operator to separate parameters from the body. Lambdas can be assigned to a delegate type (like Func<T, TResult> or Action<T>) or to an Expression<TDelegate>, and are the mechanism that makes LINQ, event handlers, and callback-based APIs readable without declaring a separate named method for every small operation. C# infers parameter types from context whenever possible, so you rarely need to annotate them explicitly in typical LINQ usage.
Cricket analogy: A lambda like runs => runs * 2 for a boundary bonus is like a scorer scribbling a quick one-off calculation on a napkin instead of writing a whole new rulebook chapter just to double a boundary's value for one over.
Syntax Forms
Lambdas come in expression-bodied form, x => x * x, which implicitly returns the expression's value, and statement-bodied form, x => { var y = x * x; return y; }, which allows multiple statements inside braces with an explicit return. Parameter lists can omit types (implicit), include them explicitly, or use var (C# 10+) for clarity in complex generic contexts. A parameterless lambda is written () => ..., and multiple parameters are wrapped in parentheses: (x, y) => x + y.
Cricket analogy: x => x * x is like a quick single-line note in the scorebook margin, 'double it', while a statement-bodied lambda with braces is like a fuller scoring annotation with several steps, ending in an explicit recorded result.
Func<int, int, int> add = (x, y) => x + y;
Action<string> log = message => Console.WriteLine($"[LOG] {message}");
Predicate<int> isEven = n => n % 2 == 0;
// Statement-bodied lambda with multiple statements
Func<int, string> classify = n =>
{
if (n < 0) return "negative";
if (n == 0) return "zero";
return "positive";
};
// Lambdas used with LINQ operators
var numbers = Enumerable.Range(-3, 7).ToList();
var positives = numbers.Where(n => n > 0).ToList();
// C# 12: natural type inference for lambdas assigned to var
var square = (int n) => n * n;
Console.WriteLine(add(2, 3));
Console.WriteLine(classify(-5));Closures and Captured Variables
Lambdas can capture variables from their enclosing scope, forming a closure. Captured variables are not copied by value at the moment the lambda is created — instead, the lambda holds a reference to the variable itself, so if the outer variable changes after the lambda is defined but before it's invoked, the lambda sees the updated value. This is especially important inside loops, where capturing the loop variable incorrectly (a mistake more common in older C# versions, though C# 5+ fixed foreach's per-iteration scoping) can lead to every closure seeing the same final value.
Cricket analogy: A lambda capturing the current over count as a closure is like a scorer's running tally that always reflects the latest over, not a frozen snapshot from when the tally was first written down at the start of play.
A lambda can be compiled either to a delegate (an actual executable function pointer) or to an Expression<TDelegate> (a data structure describing the lambda's logic as an expression tree). EF Core relies on the latter to translate LINQ predicates into SQL — this is why arbitrary C# method calls inside an EF query sometimes fail to translate: the expression tree has no way to represent them.
Capturing a mutable field or loop-created disposable object inside a lambda that outlives the current iteration (e.g., stored in a list of callbacks) can cause unexpected shared state or use of a disposed object. Prefer capturing a local copy inside the loop body if each lambda needs its own independent value.
- Lambda expressions are anonymous, inline functions written with the => operator.
- They can be expression-bodied (single expression, implicit return) or statement-bodied (block with explicit return).
- Lambdas can compile to delegates (Func/Action/Predicate) or to Expression<TDelegate> for expression-tree-based APIs like EF Core.
- Closures let lambdas capture enclosing variables by reference, not by value snapshot.
- Since C# 5, foreach gives each iteration its own loop variable, avoiding the classic closure-capture-in-loop bug.
- Type inference usually removes the need to annotate lambda parameter types explicitly.
Practice what you learned
1. What operator is used to define a lambda expression in C#?
2. How does a closure capture an outer variable in a C# lambda?
3. What is the practical difference between compiling a lambda to a delegate versus an Expression<TDelegate>?
4. Which built-in delegate type would you use for a lambda that takes an int and returns a bool?
5. What changed about foreach loop variable scoping starting with C# 5, relevant to lambda closures?
Was this page helpful?
You May Also Like
Delegates and Events
Delegates are type-safe function pointers that enable callbacks and composition; events build on delegates to provide a controlled publish-subscribe mechanism.
LINQ Basics
Language Integrated Query lets you write expressive, type-safe queries directly in C# over collections, databases, and XML using a unified syntax.
Extension Methods
Extension methods let you add new callable methods to existing types, including sealed or third-party ones, without modifying their source or subclassing.
Pattern Matching
Survey C#'s pattern matching features — type patterns, property patterns, relational and logical patterns, and switch expressions — for expressive, safe branching.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics