Delegates & Lambdas

Work with function types, closures, and expression trees

Lesson 28 of 35 available15 practice questions

ACTIVE PRACTICE Β· 15 practice questions

Make this lesson stick.

Test what you can recall and learn from the feedback. Come back to the lesson whenever you need an explanation.

Sign in to practice β†’

Last generated

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)     β”‚
β”‚         β”‚                                   β”‚
β”‚         β”œβ”€β”€β†’ Action<T> delegates            β”‚
β”‚         β”‚    (void return, 0-16 params)     β”‚
β”‚         β”‚                                   β”‚
β”‚         └──→ Func<T> 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 a Func<int, bool> predicate
  • Select(n => n * n) uses a Func<int, int> transformation
  • OrderByDescending(n => n) uses a Func<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<int> a = MyMethod; Reusable logic, complex methods
Anonymous method Action<int> a = delegate(int x) { }; Legacy code (pre-C# 3.0)
Lambda expression Action<int> a = x => { }; 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

  1. Microsoft Docs - Delegates: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/
  2. Microsoft Docs - Lambda Expressions: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
  3. 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!