Delegates & Lambdas
Work with function types, closures, and expression trees
Delegates & Lambdas in C#
Master the power of delegates and lambdas with free flashcards and spaced repetition practice to solidify your understanding. This lesson covers delegate types, lambda expressions, and functional programming patternsβessential concepts for writing flexible, maintainable C# code that leverages higher-order functions and event-driven architectures.
Welcome to Delegates & Lambdas π»
Delegates and lambdas are fundamental to modern C# programming, enabling you to write concise, expressive code that treats functions as first-class citizens. Whether you're building event handlers, implementing LINQ queries, or creating callbacks, understanding these patterns will transform how you approach problem-solving in C#.
What You'll Learn
- Delegate fundamentals: Type-safe function pointers in C#
- Lambda expressions: Anonymous functions with compact syntax
- Func and Action delegates: Built-in generic delegate types
- Closures and captured variables: How lambdas capture state
- Practical patterns: Real-world applications in LINQ, events, and callbacks
Core Concepts: Understanding Delegates π―
What Are Delegates?
A delegate is a type that represents references to methods with a specific parameter list and return type. Think of a delegate as a "function pointer" but with type safety. When you instantiate a delegate, you can associate its instance with any method that has a compatible signature.
Key characteristics:
- Type-safe function references
- Can point to static or instance methods
- Support multicast (chaining multiple methods)
- Foundation for events and callbacks
Declaring and Using Delegates
Here's the basic syntax for declaring a delegate:
// Delegate declaration
public delegate int MathOperation(int x, int y);
// Method that matches the delegate signature
public static int Add(int a, int b)
{
return a + b;
}
// Using the delegate
MathOperation operation = Add;
int result = operation(5, 3); // Returns 8
Delegate Type Hierarchy
βββββββββββββββββββββββββββββββββββββββββββββββ β DELEGATE TYPE HIERARCHY β βββββββββββββββββββββββββββββββββββββββββββββββ€ β β β System.Delegate (base class) β β β β β ββββ Custom Delegates β β β (MyDelegate, EventHandler) β β β β β ββββ Actiondelegates β β β (void return, 0-16 params) β β β β β ββββ Func delegates β β (typed return, 0-16 params) β β β βββββββββββββββββββββββββββββββββββββββββββββββ
Built-in Generic Delegates: Func and Action
C# provides built-in generic delegates that eliminate the need to declare custom delegate types in most scenarios:
| Delegate Type | Return Type | Parameters | Example |
|---|---|---|---|
| Action<T> | void | 0-16 input parameters | Action<string> print = Console.WriteLine; |
| Func<T, TResult> | TResult (last type param) | 0-16 input parameters | Func<int, int, int> add = (a, b) => a + b; |
| Predicate<T> | bool | Single T parameter | Predicate<int> isEven = x => x % 2 == 0; |
π‘ Tip: Use Action<T> for methods that return void, and Func<T, TResult> for methods that return a value. The last type parameter in Func is always the return type.
Lambda Expressions: Anonymous Functions β‘
Lambda Syntax
A lambda expression is an anonymous function written in a compact form. Lambdas use the => operator (read as "goes to" or "arrow").
Basic syntax patterns:
// Single parameter, expression body
x => x * x
// Multiple parameters, expression body
(x, y) => x + y
// No parameters
() => Console.WriteLine("Hello")
// Statement body (multiple statements)
(x, y) =>
{
var sum = x + y;
return sum * 2;
}
// Explicit type declaration
(int x, int y) => x + y
Lambda Expression Anatomy
βββββββββββββββββββββββββββββββββββββββββββββββ
β LAMBDA EXPRESSION COMPONENTS β
βββββββββββββββββββββββββββββββββββββββββββββββ
(x, y) => x + y
β β β
β β ββ Expression body
β ββ Lambda operator
ββ Parameter list
(string s) => { return s.ToUpper(); }
β β
β ββ Statement body (requires return)
ββ Explicit type (optional)
Expression Body vs Statement Body
// Expression body (implicit return)
Func<int, int> square = x => x * x;
// Statement body (explicit return required)
Func<int, int> squareVerbose = x =>
{
return x * x;
};
// Multiple statements require statement body
Func<int, string> process = x =>
{
int squared = x * x;
return $"Result: {squared}";
};
β οΈ Common Mistake: Forgetting the return keyword in statement-body lambdas!
// β WRONG - Missing return
Func<int, int> broken = x => { x * x; };
// β
CORRECT
Func<int, int> works = x => { return x * x; };
// β
OR use expression body
Func<int, int> better = x => x * x;
Closures: Capturing Variables π
Lambdas can capture variables from their enclosing scope, creating a closure. This is one of the most powerfulβand sometimes surprisingβfeatures of lambdas.
How Closures Work
public Func<int, int> CreateMultiplier(int factor)
{
// Lambda captures 'factor' from outer scope
return x => x * factor;
}
// Usage
var triple = CreateMultiplier(3);
var result = triple(5); // Returns 15
var double = CreateMultiplier(2);
result = double(5); // Returns 10
The lambda x => x * factor "closes over" the variable factor, keeping it alive even after CreateMultiplier returns.
Closure Lifetime and Mutation
// Captured variables can be modified
public Action CreateCounter()
{
int count = 0;
return () => Console.WriteLine($"Count: {++count}");
}
var counter = CreateCounter();
counter(); // Output: Count: 1
counter(); // Output: Count: 2
counter(); // Output: Count: 3
β οΈ Watch Out: Closures capture the variable itself, not its value at the time of lambda creation!
// Common gotcha with loop variables
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
// β PROBLEM: All lambdas capture same 'i' variable
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action(); // Prints: 3, 3, 3 (not 0, 1, 2!)
}
Solution: Create a local copy inside the loop:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int localCopy = i; // Create loop-scoped copy
actions.Add(() => Console.WriteLine(localCopy));
}
foreach (var action in actions)
{
action(); // Prints: 0, 1, 2 β
}
π‘ Modern C# Note: In C# 5.0+, foreach loop variables are automatically scoped per-iteration, avoiding this issue. But for loops still require the workaround!
Practical Examples π οΈ
Example 1: LINQ Query with Lambdas
Lambdas are the foundation of LINQ (Language Integrated Query), enabling powerful data transformations:
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Filter even numbers
var evens = numbers.Where(n => n % 2 == 0);
// Transform each number
var squared = numbers.Select(n => n * n);
// Chain operations
var result = numbers
.Where(n => n > 3) // Filter
.Select(n => n * 2) // Transform
.OrderByDescending(n => n) // Sort
.Take(3); // Take first 3
// Result: [20, 18, 16]
Breaking it down:
Where(n => n % 2 == 0)uses aFunc<int, bool>predicateSelect(n => n * n)uses aFunc<int, int>transformationOrderByDescending(n => n)uses aFunc<int, int>key selector
Example 2: Custom Higher-Order Function
A higher-order function takes functions as parameters or returns them:
public static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
{
result.Add(item);
}
}
return result;
}
// Usage with different predicates
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evens = Filter(numbers, x => x % 2 == 0);
var greaterThanThree = Filter(numbers, x => x > 3);
var allNumbers = Filter(numbers, x => true);
Example 3: Event Handlers with Lambdas
Lambdas provide a concise syntax for event subscription:
// Traditional event handler
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("Button clicked!");
}
// Lambda event handler (inline)
button.Click += (sender, e) => Console.WriteLine("Button clicked!");
// Lambda with captured variable
int clickCount = 0;
button.Click += (sender, e) =>
{
clickCount++;
Console.WriteLine($"Clicked {clickCount} times");
};
Example 4: Async Operations with Callbacks
Delegates and lambdas shine in asynchronous programming:
public void ProcessDataAsync(Action<string> onComplete, Action<Exception> onError)
{
Task.Run(() =>
{
try
{
// Simulate processing
Thread.Sleep(1000);
var result = "Processing complete!";
onComplete(result);
}
catch (Exception ex)
{
onError(ex);
}
});
}
// Usage with lambda callbacks
ProcessDataAsync(
result => Console.WriteLine($"Success: {result}"),
error => Console.WriteLine($"Error: {error.Message}")
);
Comparison table: Different delegate patterns
| Pattern | Syntax | Use Case |
|---|---|---|
| Named method | Action |
Reusable logic, complex methods |
| Anonymous method | Action |
Legacy code (pre-C# 3.0) |
| Lambda expression | Action |
Modern, concise, preferred |
| Local function | void Local() { } Action a = Local; | Helper methods within methods |
Common Mistakes β οΈ
1. Forgetting Return in Statement Bodies
// β WRONG - Compiler error
Func<int, int> broken = x => { x * x; };
// β
CORRECT - Explicit return
Func<int, int> works = x => { return x * x; };
// β
BETTER - Use expression body
Func<int, int> best = x => x * x;
2. Capturing Loop Variables Incorrectly
// β WRONG - All delegates capture same variable
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i)); // Captures 'i' reference
}
// β
CORRECT - Create local copy
for (int i = 0; i < 5; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
3. Misunderstanding Delegate Compatibility
// Covariance (return type)
Func<string> getString = () => "Hello";
Func<object> getObject = getString; // β OK - string is object
// Contravariance (parameter type)
Action<object> processObject = obj => Console.WriteLine(obj);
Action<string> processString = processObject; // β OK - can accept more specific
// β WRONG direction
Action<string> specific = str => Console.WriteLine(str);
Action<object> general = specific; // Compiler error!
4. Memory Leaks with Event Handlers
// β PROBLEM - Event subscription keeps object alive
public void Subscribe()
{
SomeStaticEvent += (s, e) => this.DoSomething(); // 'this' captured!
}
// β
SOLUTION - Unsubscribe when done
private EventHandler _handler;
public void Subscribe()
{
_handler = (s, e) => this.DoSomething();
SomeStaticEvent += _handler;
}
public void Unsubscribe()
{
SomeStaticEvent -= _handler;
}
5. Expensive Closures in Loops
// β INEFFICIENT - Creates new delegate each iteration
for (int i = 0; i < 1000000; i++)
{
list.ForEach(item => ProcessItem(item)); // New lambda allocated!
}
// β
BETTER - Reuse delegate
Action<Item> processor = item => ProcessItem(item);
for (int i = 0; i < 1000000; i++)
{
list.ForEach(processor);
}
// β
BEST - Use method group
for (int i = 0; i < 1000000; i++)
{
list.ForEach(ProcessItem); // No allocation
}
π§ Memory Device: "R.E.T.U.R.N. for Statement bodies" - Remember Explicit Termination: Use Return in Non-expression bodies.
Functional Programming Patterns π¨
Composing Functions
Delegates enable function composition, a key functional programming technique:
public static Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<T, TIntermediate> f,
Func<TIntermediate, TResult> g)
{
return x => g(f(x));
}
// Usage
Func<int, int> addOne = x => x + 1;
Func<int, int> square = x => x * x;
var addOneThenSquare = Compose(addOne, square);
var result = addOneThenSquare(3); // (3 + 1)Β² = 16
Partial Application
public static Func<T2, TResult> Partial<T1, T2, TResult>(
Func<T1, T2, TResult> func,
T1 arg1)
{
return arg2 => func(arg1, arg2);
}
// Usage
Func<int, int, int> multiply = (a, b) => a * b;
Func<int, int> multiplyByTen = Partial(multiply, 10);
var result = multiplyByTen(5); // Returns 50
Memoization (Caching)
public static Func<T, TResult> Memoize<T, TResult>(Func<T, TResult> func)
{
var cache = new Dictionary<T, TResult>();
return arg =>
{
if (!cache.ContainsKey(arg))
{
cache[arg] = func(arg);
}
return cache[arg];
};
}
// Usage - expensive Fibonacci calculation
Func<int, int> fib = null;
fib = n => n <= 1 ? n : fib(n - 1) + fib(n - 2);
var memoizedFib = Memoize(fib);
var result = memoizedFib(40); // Much faster on subsequent calls!
FUNCTIONAL PATTERNS FLOWCHART
π¦ Input Data
β
ββββ π§ Filter (Where)
β β
β β
β Selected items
β
ββββ π Transform (Select)
β β
β β
β Mapped items
β
ββββ π Aggregate (Reduce)
β β
β β
β Single result
β
ββββ π― Compose
β
β
Combined operations
Key Takeaways π―
β Delegates are type-safe function references that enable callbacks, events, and functional patterns
β
Use built-in generics: Action<T> for void returns, Func<T, TResult> for value returns
β
Lambda syntax: (parameters) => expression for concise anonymous functions
β
Expression bodies implicitly return; statement bodies require explicit return
β Closures capture variables, not valuesβwatch out in loops!
β
LINQ is built on lambdas: Where, Select, OrderBy, etc. all use Func<T> delegates
β Memory management: Unsubscribe event handlers to prevent leaks
β
Performance: Method groups (like MyMethod without parentheses) avoid allocations
β Functional patterns: Composition, partial application, and memoization leverage delegates
π Quick Reference Card
Delegates & Lambdas Cheat Sheet
| Concept | Syntax | Example |
|---|---|---|
| Custom Delegate | delegate TResult Name(T arg); | delegate int Math(int x, int y); |
| Action (void) | Action<T> | Action<string> log = Console.WriteLine; |
| Func (returns value) | Func<T, TResult> | Func<int, bool> isEven = x => x % 2 == 0; |
| Expression Lambda | (args) => expression | x => x * x |
| Statement Lambda | (args) => { statements; return; } | x => { int y = x * 2; return y; } |
| No Parameters | () => expression | () => DateTime.Now |
| LINQ Where | collection.Where(predicate) | nums.Where(n => n > 5) |
| LINQ Select | collection.Select(transform) | nums.Select(n => n * 2) |
| Method Group | MethodName (no parens) | list.ForEach(Console.WriteLine) |
| Closure | Lambda captures outer variable | int x = 5; Func<int> f = () => x; |
π§ Remember: Lambdas are syntactic sugar for delegates. Every lambda can be rewritten as a delegate, but lambdas are more concise!
π Further Study
- Microsoft Docs - Delegates: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/
- Microsoft Docs - Lambda Expressions: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
- C# in Depth - Closures: https://csharpindepth.com/articles/Closures
π Congratulations! You've mastered delegates and lambdasβpowerful tools that will make your C# code more expressive, maintainable, and functional. Practice writing your own higher-order functions and explore how LINQ uses these patterns under the hood!