C# Lambdas — Lambda Syntax, Func/Action/Predicate, Closures, and Expression Trees
In this tutorial, you will learn about C# Lambdas. We cover key concepts, practical examples, and best practices to help you master this topic.
C# lambda expressions are anonymous functions that provide a concise syntax for defining inline method bodies, supporting closures over captured variables and compilation into both delegates and expression trees.
What You'll Learn
You will master lambdas in C#: statement lambdas and expression lambdas, the Func, Action, and Predicate delegate types that lambdas work with, closures and variable capture, expression trees for query translation, and how lambdas power .NET features like LINQ and async programming.
Why It Matters
Lambdas are the backbone of LINQ, event handling, async programming, and functional programming patterns in C#. They eliminate boilerplate by letting you define behavior at the point of use. Understanding closures — how lambdas capture variables — is critical for avoiding bugs. Expression trees enable LINQ providers like EF Core to translate C# code to SQL.
Real-World Use
LINQ queries use lambdas for filtering (Where), projection (Select), and sorting (OrderBy). ASP.NET Core uses lambdas for route handlers, middleware configuration, and Dependency Injection. Event handlers are often defined as lambdas. Task continuations use lambdas. Configuration and options patterns use lambdas extensively.
Learning Path
graph LR
A["23: Delegates & Events"] --> B["24: Lambdas"]
B --> C["25: Extension Methods"]
C --> D["26: Nullable Reference Types"]
D --> E["27: Pattern Matching"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Lambda Syntax
// Expression lambda: single expression, no braces or return
Func<int, int> square = x => x * x;
// Statement lambda: multiple statements in braces
Func<int, int> factorial = n =>
{
int result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
};
// Multiple parameters
Func<int, int, int> add = (a, b) => a + b;
// No parameters
Action greet = () => Console.WriteLine("Hello!");
// Explicit parameter types
Func<int, int, string> format = (int a, int b) => $"{a} + {b} = {a + b}";
// Discard parameters (C# 9+)
Func<int, int, int> ignore = (_, _) => 42;
Func, Action, Predicate
// Func: 0-16 input params, 1 output (last type param)
Func<int> zeroParam = () => 42;
Func<int, string> oneParam = n => $"Number: {n}";
Func<int, int, bool> twoParams = (a, b) => a == b;
// Action: 0-16 input params, void return
Action logTime = () => Console.WriteLine(DateTime.Now);
Action<string> logMessage = msg => Console.WriteLine($"LOG: {msg}");
Action<string, int> logDetail = (msg, level) =>
Console.WriteLine($"[{level}] {msg}");
// Predicate: 1 input param, bool return (equivalent to Func<T, bool>)
Predicate<int> isPositive = x => x > 0;
Predicate<string> isNullOrEmpty = string.IsNullOrEmpty;
Lambdas with LINQ
var numbers = new[] { 5, 2, 8, 1, 9, 3, 7, 4, 6 };
// Basic operations
var evens = numbers.Where(n => n % 2 == 0);
var doubled = numbers.Select(n => n * 2);
var sorted = numbers.OrderBy(n => n);
// Multiple operations
var result = numbers
.Where(n => n > 3)
.OrderByDescending(n => n)
.Select(n => $"Number: {n}^2 = {n * n}");
Console.WriteLine("Processed numbers:");
foreach (var item in result)
Console.WriteLine($" {item}");
// Grouping with lambdas
var grouped = numbers
.GroupBy(n => n % 2 == 0 ? "Even" : "Odd")
.Select(g => $"{g.Key}: {string.Join(", ", g)}");
Console.WriteLine("\nGrouped:");
foreach (var g in grouped)
Console.WriteLine($" {g}");
Closures (Variable Capture)
Lambdas capture the variables in their enclosing scope:
int multiplier = 5;
Func<int, int> times = x => x * multiplier;
Console.WriteLine(times(3)); // 15
multiplier = 10;
Console.WriteLine(times(3)); // 30 (closure captures the variable, not the value)
Captured Variables Pitfall (Loop Variable)
// Problem: all lambdas capture the same variable
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // Outputs: 5 5 5 5 5 (not 0 1 2 3 4)
// Fix: capture a copy in each iteration
var fixedActions = new List<Action>();
for (int i = 0; i < 5; i++)
{
int captured = i; // New variable per iteration
fixedActions.Add(() => Console.WriteLine(captured));
}
foreach (var action in fixedActions)
action(); // Outputs: 0 1 2 3 4
Static Lambdas (C# 9+)
// Static lambda: explicitly does not capture variables
int notCaptured = 42;
Func<int, int> staticLambda = static x => x * x; // Cannot use notCaptured here
// staticLambda = static x => x * notCaptured; // Compilation error!
Local Functions vs Lambdas
// Lambda
Func<int, int> fibonacciLambda = n =>
{
if (n <= 1) return n;
return fibonacciLambda(n - 1) + fibonacciLambda(n - 2); // Requires reassignment
};
// Local function (preferred for recursion and generics)
int Fibonacci(int n)
{
if (n <= 1) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Console.WriteLine(Fibonacci(10)); // 55
Expression Trees
Expression trees represent code as data (Abstract Syntax Tree):
using System.Linq.Expressions;
// Expression tree (not a delegate)
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// Compile to delegate
Func<int, int, int> addFunc = addExpr.Compile();
Console.WriteLine(addFunc(3, 4)); // 7
// Inspect the expression tree
Console.WriteLine($"Node type: {addExpr.NodeType}"); // Lambda
Console.WriteLine($"Body type: {addExpr.Body.NodeType}"); // Add
Console.WriteLine($"Body: {addExpr.Body}"); // (a + b)
var binary = (BinaryExpression)addExpr.Body;
Console.WriteLine($"Left: {binary.Left}"); // a
Console.WriteLine($"Right: {binary.Right}"); // b
Expression Trees in LINQ Providers
// IQueryable<T> uses Expression trees (translated to SQL by EF Core)
// IEnumerable<T> uses delegates (executed in-memory)
// This creates an Expression tree:
IQueryable<Product> query = dbContext.Products
.Where(p => p.Price > 100) // Stored as Expression tree
.OrderBy(p => p.Name);
// This creates a delegate:
IEnumerable<Product> localQuery = products
.Where(p => p.Price > 100); // Compiled to delegate
Functional Composition with Lambdas
// Pipeline composition
Func<int, int> pipeline =
((Func<int, int>)(x => x * 2))
.Compose(x => x + 1)
.Compose(x => x.ToString().Length)
.Compose(x => x * x);
// Custom compose extension
static class FuncExtensions
{
public static Func<T, T> Compose<T>(this Func<T, T> f, Func<T, T> g)
=> x => g(f(x));
}
Console.WriteLine(pipeline(5)); // ((5*2)+1)=11, length=2, 2*2=4
// Partial application
Func<int, int, int> add = (a, b) => a + b;
Func<int, int> add5 = x => add(x, 5);
Console.WriteLine(add5(10)); // 15
Common Mistakes
Mistake 1: Capturing Loop Variables Incorrectly
In foreach loops (before C# 5), the loop variable was captured by reference. In modern C#, foreach captures correctly. But for loops still have the issue. Use a local copy.
Mistake 2: Not Understanding Closure Lifetime
Captured variables extend the lifetime of local variables. A lambda that captures a large object prevents it from being garbage collected until the lambda itself is collected.
Mistake 3: Using Statement Lambdas When Expression Lambdas Suffice
x => x * x is preferable to x => { return x * x; }. Expression lambdas are more concise and can be used as expression trees.
Mistake 4: Confusing Expression Trees with Delegates
Expression trees represent code as data (analyzed at runtime). Delegates are compiled code (executed directly). LINQ providers use expression trees for translation.
Mistake 5: Using a Lambda Where a Method Group Works
list.Where(IsEven) (method group) is often cleaner than list.Where(x => IsEven(x)). The compiler converts the method group to a delegate automatically.
Mistake 6: Modifying Captured Variables After Creating the Lambda
The lambda sees the latest value of captured variables, not the value at creation time. This causes bugs when the variable changes before the lambda executes.
Practice Questions
- What is the difference between an expression lambda and a statement lambda?
- How do closures work? What happens when a lambda captures a variable?
- What are expression trees and why are they important for LINQ providers?
- Why was the loop variable capture behavior changed in C# 5?
- Write a function that takes a list of integers and returns the sum of squares using lambdas.
Challenge
Build a simple LINQ-like query pipeline using only lambdas and extension methods. Implement Where, Select, and Aggregate as extension methods on IEnumerable
FAQ
Mini Project
Create a functional data processing pipeline:
public static class Pipeline
{
public static Func<T, T> Compose<T>(params Func<T, T>[] steps)
{
return input =>
{
T result = input;
foreach (var step in steps)
result = step(result);
return result;
};
}
}
public class Transaction
{
public decimal Amount { get; set; }
public string Currency { get; set; } = "USD";
public decimal Fee { get; set; }
public bool IsProcessed { get; set; }
}
// Processing steps
static class TransactionSteps
{
public static Func<Transaction, Transaction> Validate = t =>
{
if (t.Amount <= 0)
throw new InvalidOperationException("Amount must be positive");
Console.WriteLine($" Validated: ${t.Amount}");
return t;
};
public static Func<Transaction, Transaction> ApplyFee = t =>
{
t.Fee = t.Amount * 0.025m;
Console.WriteLine($" Fee applied: ${t.Fee:F2}");
return t;
};
public static Func<Transaction, Transaction> ConvertCurrency = t =>
{
if (t.Currency != "USD")
{
t.Amount *= 1.12m; // Conversion rate
t.Currency = "USD";
Console.WriteLine($" Converted to USD: ${t.Amount:F2}");
}
return t;
};
public static Func<Transaction, Transaction> Process = t =>
{
t.IsProcessed = true;
Console.WriteLine($" Processed net: ${t.Amount - t.Fee:F2}");
return t;
};
}
var pipeline = Pipeline.Compose(
TransactionSteps.Validate,
TransactionSteps.ConvertCurrency,
TransactionSteps.ApplyFee,
TransactionSteps.Process
);
var transaction = new Transaction { Amount = 200, Currency = "EUR" };
Console.WriteLine("Processing transaction pipeline:");
var result = pipeline(transaction);
Console.WriteLine($"\nResult: Processed={result.IsProcessed}, Net=${result.Amount - result.Fee:F2}");
Expected output:
Processing transaction pipeline:
Validated: $200
Converted to USD: $224.00
Fee applied: $5.60
Processed net: $218.40
Result: Processed=True, Net=$218.40
What's Next
You have mastered lambdas in C# including closures and expression trees. The next lesson covers extension methods: defining static extension methods and how LINQ uses them.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro