You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

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)     β”‚
β”‚         β”‚                                   β”‚
β”‚         β”œβ”€β”€β†’ Action delegates            β”‚
β”‚         β”‚    (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 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 a = MyMethod; Reusable logic, complex methods
Anonymous method Action a = delegate(int x) { }; Legacy code (pre-C# 3.0)
Lambda expression Action 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!