Why Lambda Expressions Transform Your Code
Lambda expressions let you write inline functions without the ceremony of declaring separate methods. They make your code more readable by keeping logic close to where you use it, especially when working with collections, LINQ queries, and event handlers.
Modern C# development relies heavily on functional programming patterns. Lambda expressions are the foundation of LINQ, async callbacks, and functional-style operations like map, filter, and reduce. You'll use them daily when working with lists, databases, or any code that needs to pass behavior as a parameter.
This guide shows you how to create lambda expressions, work with anonymous functions and delegates, understand closures, and apply these patterns effectively in real applications.
Understanding Delegates First
Before mastering lambdas, you need to understand delegates. A delegate is a type that represents a method signature. You can assign any method with a matching signature to a delegate variable, enabling you to pass methods as parameters.
// Declare a delegate type
public delegate int MathOperation(int x, int y);
// Method that matches the delegate signature
public static int Add(int x, int y)
{
return x + y;
}
public static int Multiply(int x, int y)
{
return x * y;
}
// Using delegates
MathOperation operation = Add;
int result1 = operation(5, 3); // Returns 8
operation = Multiply;
int result2 = operation(5, 3); // Returns 15
Console.WriteLine($"Add: {result1}, Multiply: {result2}");
Delegates let you treat methods as first-class objects. You can store them in variables, pass them to other methods, and change which method a delegate points to at runtime.
public static void ProcessNumbers(int[] numbers, MathOperation operation)
{
for (int i = 0; i < numbers.Length - 1; i++)
{
int result = operation(numbers[i], numbers[i + 1]);
Console.WriteLine($"{numbers[i]} op {numbers[i + 1]} = {result}");
}
}
int[] values = { 10, 5, 8, 3 };
Console.WriteLine("Using Add:");
ProcessNumbers(values, Add);
Console.WriteLine("\nUsing Multiply:");
ProcessNumbers(values, Multiply);
This pattern separates the operation logic from the iteration logic. You can easily add new operations without changing ProcessNumbers.
Lambda Expression Syntax
Lambda expressions provide a concise way to create anonymous functions inline. The basic syntax uses the lambda operator => which reads as "goes to" or "becomes."
// Expression lambda (single expression)
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(5, 3)); // Output: 8
// Statement lambda (multiple statements)
Func<int, int, int> addWithLogging = (x, y) =>
{
Console.WriteLine($"Adding {x} and {y}");
int result = x + y;
Console.WriteLine($"Result: {result}");
return result;
};
// Single parameter (parentheses optional)
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // Output: 16
// No parameters
Func<string> getMessage = () => "Hello, World!";
Console.WriteLine(getMessage()); // Output: Hello, World!
// Multiple parameters with explicit types
Func<string, int, string> repeat = (string text, int count) =>
{
return string.Concat(Enumerable.Repeat(text, count));
};
Console.WriteLine(repeat("Hi ", 3)); // Output: Hi Hi Hi
Expression lambdas return the result of a single expression automatically. Statement lambdas need curly braces and explicit return statements when returning values.
Working with Func and Action Delegates
The .NET Framework includes built-in generic delegates that cover most scenarios. Func represents functions that return values, while Action represents functions that perform operations without returning anything.
// Func - takes T, returns TResult
Func<int, bool> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False
// Func with multiple parameters
Func<string, string, bool> contains = (text, search) => text.Contains(search);
Console.WriteLine(contains("Hello World", "World")); // True
// Action - takes T, returns nothing
Action<string> log = message => Console.WriteLine($"[LOG] {message}");
log("Application started");
// Action with multiple parameters
Action<string, int> printMultiple = (text, count) =>
{
for (int i = 0; i < count; i++)
{
Console.WriteLine($"{i + 1}: {text}");
}
};
printMultiple("Test", 3);
Using Func and Action eliminates the need to declare custom delegate types. These generic delegates handle up to 16 parameters, covering virtually all practical scenarios.
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Filter with lambda
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
Console.WriteLine($"Even: {string.Join(", ", evenNumbers)}");
// Transform with lambda
var squared = numbers.Select(n => n * n).ToList();
Console.WriteLine($"Squared: {string.Join(", ", squared)}");
// Aggregate with lambda
int sum = numbers.Aggregate((acc, n) => acc + n);
Console.WriteLine($"Sum: {sum}");
// ForEach with lambda
numbers.ForEach(n => Console.Write($"{n} "));
// Complex filtering and transformation
var result = numbers
.Where(n => n > 3)
.Select(n => n * 2)
.OrderByDescending(n => n)
.Take(3)
.ToList();
Console.WriteLine($"\nProcessed: {string.Join(", ", result)}");
LINQ methods like Where, Select, and Aggregate expect lambda expressions. This integration makes data transformation pipelines readable and expressive.
Anonymous Methods vs Lambda Expressions
Before lambda expressions were introduced in C# 3.0, developers used anonymous methods. While lambdas are now preferred, you'll encounter anonymous methods in older code.
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Anonymous method (older style)
var evenOld = numbers.FindAll(delegate(int n)
{
return n % 2 == 0;
});
// Lambda expression (modern style)
var evenNew = numbers.FindAll(n => n % 2 == 0);
// Both produce the same result
Console.WriteLine($"Old style: {string.Join(", ", evenOld)}");
Console.WriteLine($"New style: {string.Join(", ", evenNew)}");
// Anonymous method with event handler
Button button = new Button();
button.Click += delegate(object sender, EventArgs e)
{
Console.WriteLine("Button clicked (anonymous method)");
};
// Lambda version (more concise)
button.Click += (sender, e) => Console.WriteLine("Button clicked (lambda)");
Lambda expressions offer cleaner syntax with type inference. The compiler figures out parameter types from context, reducing boilerplate. Always prefer lambdas in new code.
Understanding Closures and Variable Capture
Lambda expressions can access variables from the surrounding scope, creating closures. The captured variables remain accessible even after the enclosing method finishes, which has important implications.
public static Func<int> CreateCounter()
{
int count = 0; // This variable is captured
return () =>
{
count++; // Lambda accesses and modifies captured variable
return count;
};
}
// Usage
var counter1 = CreateCounter();
Console.WriteLine(counter1()); // 1
Console.WriteLine(counter1()); // 2
Console.WriteLine(counter1()); // 3
var counter2 = CreateCounter();
Console.WriteLine(counter2()); // 1 (separate closure)
Console.WriteLine(counter1()); // 4 (original continues)
Each call to CreateCounter creates a new closure with its own captured count variable. The lambda keeps this variable alive even though CreateCounter has finished executing.
var actions = new List<Action>();
// Problematic: all lambdas capture the same variable
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.Write($"{i} "));
}
// All print 5 because they captured the loop variable
foreach (var action in actions)
{
action(); // Output: 5 5 5 5 5
}
Console.WriteLine();
// Solution: capture a copy of the loop variable
var correctActions = new List<Action>();
for (int i = 0; i < 5; i++)
{
int copy = i; // Create a copy for each iteration
correctActions.Add(() => Console.Write($"{copy} "));
}
foreach (var action in correctActions)
{
action(); // Output: 0 1 2 3 4
}
Loop variables are captured by reference, not by value. When the loop finishes, all lambdas see the final value. Creating a loop-scoped copy captures the value at each iteration.
Practical Lambda Patterns
Lambda expressions enable several powerful patterns that make your code more maintainable and expressive.
public class Calculator
{
private Dictionary<string, Func<double, double, double>> operations;
public Calculator()
{
operations = new Dictionary<string, Func<double, double, double>>
{
["add"] = (x, y) => x + y,
["subtract"] = (x, y) => x - y,
["multiply"] = (x, y) => x * y,
["divide"] = (x, y) => y != 0 ? x / y : throw new DivideByZeroException()
};
}
public double Execute(string operation, double x, double y)
{
if (operations.TryGetValue(operation, out var func))
{
return func(x, y);
}
throw new ArgumentException($"Unknown operation: {operation}");
}
public void AddOperation(string name, Func<double, double, double> operation)
{
operations[name] = operation;
}
}
// Usage
var calc = new Calculator();
Console.WriteLine(calc.Execute("add", 10, 5)); // 15
Console.WriteLine(calc.Execute("multiply", 10, 5)); // 50
// Add custom operation
calc.AddOperation("power", (x, y) => Math.Pow(x, y));
Console.WriteLine(calc.Execute("power", 2, 8)); // 256
This pattern uses lambdas to implement strategies without creating separate classes for each operation. Adding new operations becomes trivial.
public static void LogMessage(string level, Func<string> messageFactory)
{
// Only call messageFactory if logging is enabled
if (IsLoggingEnabled(level))
{
string message = messageFactory(); // Compute message only when needed
Console.WriteLine($"[{level}] {message}");
}
}
public static bool IsLoggingEnabled(string level)
{
return level == "ERROR" || level == "WARNING";
}
// Usage
var user = new { Id = 123, Name = "John" };
// Expensive operation only executes if logging is enabled
LogMessage("DEBUG", () => $"User {user.Id} details: {GetExpensiveUserDetails(user)}");
LogMessage("ERROR", () => $"Failed to process user {user.Id}");
static string GetExpensiveUserDetails(dynamic user)
{
Console.WriteLine("Computing expensive details...");
return $"Full profile for {user.Name}";
}
Wrapping expensive operations in lambdas defers their execution until needed. This pattern improves performance by avoiding unnecessary computations.
Lambda Expressions in LINQ Queries
LINQ queries become powerful when combined with lambda expressions. You can chain operations to build complex data transformations declaratively.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public int Stock { get; set; }
}
var products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999, Category = "Electronics", Stock = 5 },
new Product { Id = 2, Name = "Mouse", Price = 25, Category = "Electronics", Stock = 50 },
new Product { Id = 3, Name = "Desk", Price = 299, Category = "Furniture", Stock = 10 },
new Product { Id = 4, Name = "Chair", Price = 199, Category = "Furniture", Stock = 15 },
new Product { Id = 5, Name = "Monitor", Price = 349, Category = "Electronics", Stock = 0 }
};
// Complex query with multiple lambda expressions
var expensiveInStock = products
.Where(p => p.Stock > 0)
.Where(p => p.Price > 100)
.OrderByDescending(p => p.Price)
.Select(p => new
{
p.Name,
p.Price,
DiscountPrice = p.Price * 0.9m,
p.Stock
})
.ToList();
Console.WriteLine("Expensive in-stock items:");
expensiveInStock.ForEach(p =>
Console.WriteLine($"{p.Name}: ${p.Price} (Sale: ${p.DiscountPrice:F2})"));
// Grouping with lambdas
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
TotalValue = g.Sum(p => p.Price * p.Stock)
})
.ToList();
Console.WriteLine("\nCategory summary:");
byCategory.ForEach(c =>
Console.WriteLine($"{c.Category}: {c.Count} items, avg ${c.AvgPrice:F2}"));
Lambda expressions make LINQ queries readable while performing complex transformations. Each operation clearly expresses its intent through inline logic.