Delegates and Events

Master C# delegates, events, and event-driven programming including Action, Func, lambda expressions, and the observer pattern

C#
Delegates
Events
Functional Programming
22 min read

Overview

Delegates and events are the foundation of callbacks, LINQ, asynchronous patterns, and UI event handling in .NET. Understanding them deeply enables effective use of frameworks and creation of extensible APIs.

Core Concepts

Custom Delegates

// Delegate declaration
public delegate int Calculator(int x, int y);
public delegate void Logger(string message, LogLevel level);
public delegate T Factory<T>() where T : new();

// Methods matching delegate signatures
public int Add(int a, int b) => a + b;
public int Multiply(int a, int b) => a * b;

// Using delegates
Calculator calc = Add;
Console.WriteLine(calc(3, 4));  // 7

calc = Multiply;
Console.WriteLine(calc(3, 4));  // 12

// Multicast delegate (multiple methods)
Calculator multi = Add;
multi += Multiply;  // Add to invocation list

// Note: For non-void delegates, only last return value is kept
int result = multi(3, 4);  // Returns 12 (Multiply's result)

// Iterating multicast delegate
foreach (Calculator d in multi.GetInvocationList())
{
    Console.WriteLine(d(3, 4));
}

Built-in Delegates

// Action - void return
Action greet = () => Console.WriteLine("Hello!");
Action<string> greetPerson = name => Console.WriteLine($"Hello, {name}!");
Action<string, int> greetWithAge = (name, age) =>
    Console.WriteLine($"{name} is {age}");

greet();
greetPerson("Alice");
greetWithAge("Bob", 30);

// Func - returns value
Func<int> getNumber = () => 42;
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> getLength = s => s.Length;

Console.WriteLine(getNumber());     // 42
Console.WriteLine(square(5));       // 25
Console.WriteLine(add(3, 4));       // 7

// Predicate - returns bool (equivalent to Func<T, bool>)
Predicate<int> isPositive = x => x > 0;
Predicate<string> isEmpty = string.IsNullOrEmpty;

Console.WriteLine(isPositive(5));   // true
Console.WriteLine(isEmpty(""));     // true

// Using with LINQ
var numbers = new[] { 1, 2, 3, 4, 5 };
var evens = numbers.Where(x => x % 2 == 0);  // Func<int, bool>
var doubled = numbers.Select(x => x * 2);    // Func<int, int>

Lambda Expressions

// Expression lambda (single expression)
Func<int, int> square = x => x * x;
Func<int, int, int> add = (x, y) => x + y;

// Statement lambda (multiple statements)
Func<int, int> factorial = n =>
{
    int result = 1;
    for (int i = 2; i <= n; i++)
        result *= i;
    return result;
};

// Lambda with explicit types
Func<int, string> convert = (int x) => x.ToString();

// Capturing variables (closure)
int multiplier = 3;
Func<int, int> multiplyBy = x => x * multiplier;
Console.WriteLine(multiplyBy(5));  // 15

multiplier = 10;
Console.WriteLine(multiplyBy(5));  // 50 (captures variable, not value)

// Static lambda (C# 9) - cannot capture
Func<int, int> staticLambda = static x => x * 2;

Events

public class Order
{
    // Event using EventHandler<T>
    public event EventHandler<OrderEventArgs>? StatusChanged;

    private OrderStatus _status;
    public OrderStatus Status
    {
        get => _status;
        set
        {
            if (_status != value)
            {
                var oldStatus = _status;
                _status = value;
                OnStatusChanged(oldStatus, value);
            }
        }
    }

    // Protected virtual method to raise event (standard pattern)
    protected virtual void OnStatusChanged(OrderStatus oldStatus, OrderStatus newStatus)
    {
        StatusChanged?.Invoke(this, new OrderEventArgs(oldStatus, newStatus));
    }
}

public class OrderEventArgs : EventArgs
{
    public OrderStatus OldStatus { get; }
    public OrderStatus NewStatus { get; }

    public OrderEventArgs(OrderStatus oldStatus, OrderStatus newStatus)
    {
        OldStatus = oldStatus;
        NewStatus = newStatus;
    }
}

public enum OrderStatus { Pending, Processing, Shipped, Delivered }

// Usage
var order = new Order();

// Subscribe with lambda
order.StatusChanged += (sender, e) =>
{
    Console.WriteLine($"Order changed from {e.OldStatus} to {e.NewStatus}");
};

// Subscribe with method
order.StatusChanged += HandleStatusChanged;

order.Status = OrderStatus.Processing;
order.Status = OrderStatus.Shipped;

void HandleStatusChanged(object? sender, OrderEventArgs e)
{
    Console.WriteLine($"[Handler] Status is now: {e.NewStatus}");
}

Custom Event Accessors

public class SafeEventPublisher
{
    // Thread-safe event with custom accessors
    private EventHandler? _myEvent;
    private readonly object _lock = new();

    public event EventHandler MyEvent
    {
        add
        {
            lock (_lock)
            {
                _myEvent += value;
            }
        }
        remove
        {
            lock (_lock)
            {
                _myEvent -= value;
            }
        }
    }

    protected virtual void OnMyEvent()
    {
        EventHandler? handler;
        lock (_lock)
        {
            handler = _myEvent;
        }
        handler?.Invoke(this, EventArgs.Empty);
    }
}

Delegates as Method Parameters

public class DataProcessor
{
    // Method accepting delegate parameter
    public IEnumerable<T> Filter<T>(IEnumerable<T> items, Func<T, bool> predicate)
    {
        foreach (var item in items)
        {
            if (predicate(item))
                yield return item;
        }
    }

    public IEnumerable<TResult> Transform<T, TResult>(
        IEnumerable<T> items,
        Func<T, TResult> selector)
    {
        foreach (var item in items)
        {
            yield return selector(item);
        }
    }

    public void ForEach<T>(IEnumerable<T> items, Action<T> action)
    {
        foreach (var item in items)
        {
            action(item);
        }
    }
}

// Usage
var processor = new DataProcessor();
var numbers = new[] { 1, 2, 3, 4, 5 };

var evens = processor.Filter(numbers, n => n % 2 == 0);
var squared = processor.Transform(numbers, n => n * n);
processor.ForEach(numbers, n => Console.WriteLine(n));

Interview Tips

Tip 1: Know the difference between delegate and event: delegates can be invoked by any code with access; events can only be raised by the declaring class.

Tip 2: Understand closures: lambdas capture variables, not values. This is a common source of bugs in loops.

Tip 3: Events should always be raised using the null-conditional operator (?.Invoke) for thread safety.

Common Interview Questions

  1. What is a delegate?

    • A type-safe function pointer that holds references to methods with a specific signature. Can be invoked like a method. Supports multicast (multiple methods).
  2. What's the difference between delegate and event?

    • Delegate: Can be invoked by anyone, can be assigned (=). Event: Can only be raised by declaring class, can only add/remove (+=/-=). Events are encapsulated delegates that prevent external invocation.
  3. What are Action and Func?

    • Built-in generic delegates. Action: void return, 0-16 parameters. Func: has return type (last type parameter), 0-16 input parameters. Eliminate need for custom delegate types.
  4. What is a closure in C#?

    • When a lambda captures variables from its enclosing scope. The variable is captured by reference, so changes affect the lambda. Can cause unexpected behavior if captured variable changes.
  5. How do you unsubscribe from an event?

    • Use -= operator: button.Click -= HandleClick;. Important for preventing memory leaks. Anonymous lambdas can't be unsubscribed (no reference to original delegate).
  6. Why use null-conditional operator with events?

    • Thread safety: between null check and invocation, another thread could unsubscribe, causing NullReferenceException. Event?.Invoke() is atomic.
  7. What is a multicast delegate?

    • Delegate holding references to multiple methods. When invoked, all methods are called in order. Built on delegate's GetInvocationList(). All delegates in C# are multicast capable.