Delegates and Events
Definition
A delegate is a type-safe function pointer that references a method with a specific signature. Delegates enable passing methods as arguments, storing method references, and building callback mechanisms.
An event is a notification mechanism built on delegates that implements the observer (pub/sub) pattern. Events restrict external code to only subscribing or unsubscribing — they cannot invoke the event from outside the declaring class.
Core Concepts
Delegate Declaration and Invocation
// Declare a delegate type
public delegate int MathOperation(int a, int b);
// Methods that match the signature
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
// Instantiate and invoke
MathOperation op = Add;
int result = op(3, 4); // result = 7
op = Multiply;
result = op(3, 4); // result = 12
Multicast Delegates
Delegates are inherently multicast — they hold an invocation list of methods that execute in order.
public delegate void NotifyHandler(string message);
NotifyHandler handler = msg => Console.WriteLine($"Email: {msg}");
handler += msg => Console.WriteLine($"SMS: {msg}");
handler += msg => Console.WriteLine($"Push: {msg}");
handler("Order shipped");
// Output:
// Email: Order shipped
// SMS: Order shipped
// Push: Order shipped
// Inspect invocation list
Delegate[] list = handler.GetInvocationList();
Console.WriteLine(list.Length); // 3
// Remove a handler
handler -= msg => Console.WriteLine($"SMS: {msg}"); // removes first match
When invoking a multicast delegate with a non-void return type, only the last method's return value is returned. Prefer void return types for multicast delegates, or iterate GetInvocationList() manually.
Built-in Delegate Types
| Delegate | Signature | Use Case |
|---|---|---|
Action | void() | No-input callback |
Action<T> | void(T) | Single-input callback |
Action<T1, T2> | void(T1, T2) | Multi-input callback (up to 16 params) |
Func<TResult> | TResult() | No-input, returns value |
Func<T, TResult> | TResult(T) | Transform/filter |
Predicate<T> | bool(T) | True/false test |
// Action — void return
Action<string> log = message => Console.WriteLine(message);
log("Hello");
// Func — typed return
Func<int, int, int> add = (a, b) => a + b;
int sum = add(2, 3);
// Predicate — bool return
Predicate<int> isEven = n => n % 2 == 0;
bool result = isEven(4); // true
Anonymous Methods
// Anonymous method using delegate keyword
Func<int, int, int> subtract = delegate(int a, int b)
{
return a - b;
};
Lambda Expressions
Lambdas are concise inline delegate definitions using the => operator.
// Expression lambda — single expression
Func<int, int> doubleIt = x => x * 2;
// Statement lambda — block of statements
Action<string> greet = name =>
{
string message = $"Hello, {name}!";
Console.WriteLine(message);
};
// Lambda with multiple parameters
Func<int, int, bool> areEqual = (a, b) => a == b;
// Discard parameters when unused
Action<int, int> logFirst = (_, _) => Console.WriteLine("called");
Closures and Captured Variables
Lambdas can capture variables from their enclosing scope. The captured variable's lifetime extends to match the delegate's lifetime.
public static Func<int> CreateCounter()
{
int count = 0; // captured variable
return () => ++count; // closure over count
}
var counter = CreateCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
Capturing a loop variable in a lambda can cause unexpected behavior because the lambda captures the variable, not its value at the time of capture.
// BUG — all lambdas share the same 'i' variable
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();
// Output: 3, 3, 3 (not 0, 1, 2)
// FIX — introduce a local copy inside the loop
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
// Output: 0, 1, 2
Starting with C# 5, foreach loop variables are correctly captured per iteration, but for loop variables are not.
Events
Events encapsulate delegates, restricting external code to only += (subscribe) and -= (unsubscribe).
public class Button
{
// Event declaration using built-in EventHandler
public event EventHandler? Clicked;
public void Click()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// Subscribe
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");
button.Click(); // prints: Button clicked!
Standard Event Pattern
The standard .NET event pattern uses a custom EventArgs subclass, a protected virtual method to raise the event, and follows naming conventions.
// 1. Custom EventArgs
public class OrderCreatedEventArgs : EventArgs
{
public int OrderId { get; }
public string CustomerName { get; }
public decimal Total { get; }
public OrderCreatedEventArgs(int orderId, string customerName, decimal total)
{
OrderId = orderId;
CustomerName = customerName;
Total = total;
}
}
// 2. Publisher class
public class OrderService
{
// Event declaration
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
// Protected virtual method for raising the event
protected virtual void OnOrderCreated(OrderCreatedEventArgs e)
{
OrderCreated?.Invoke(this, e);
}
public void CreateOrder(int orderId, string customer, decimal total)
{
// ... order creation logic ...
OnOrderCreated(new OrderCreatedEventArgs(orderId, customer, total));
}
}
// 3. Subscriber
public class EmailNotifier
{
public void Subscribe(OrderService service)
{
service.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
{
Console.WriteLine($"Sending email for order {e.OrderId} to {e.CustomerName}");
}
}
Event Accessors (add/remove)
You can customize how subscribers are stored by providing explicit add and remove accessors.
public class PropertyChangedEventArgs : EventArgs
{
public string PropertyName { get; }
public PropertyChangedEventArgs(string name) => PropertyName = name;
}
public class ViewModel
{
private EventHandler<PropertyChangedEventArgs>? _propertyChanged;
public event EventHandler<PropertyChangedEventArgs> PropertyChanged
{
add => _propertyChanged += value;
remove => _propertyChanged -= value;
}
protected void OnPropertyChanged(string propertyName) =>
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Delegates vs Events
| Feature | Delegate | Event |
|---|---|---|
| External invocation | Allowed | Only from declaring class |
| External assignment | = allowed | Only += and -= |
| Null safety | Must check before invoke | Same, but encapsulated |
| Purpose | Callback, strategy pattern | Observer/publisher-subscriber |
| Interface compatibility | Can be in interface | Can be in interface |
- Delegate — when you need to pass a callback, implement the strategy pattern, or let a consumer provide a single handler.
- Event — when multiple subscribers need to react to something that happened, and only the owner should trigger it.
- Interface — when the callback requires multiple methods or carries additional contract obligations.
Common Pitfalls
Event Handler Memory Leaks
Forgetting to unsubscribe from events keeps the subscriber alive through the delegate reference, preventing garbage collection.
public class LeakExample
{
public void Subscribe(Button button)
{
button.Clicked += OnClick;
}
// If LeakExample is discarded but button still lives,
// the delegate holds a reference to LeakExample — memory leak!
public void Unsubscribe(Button button)
{
button.Clicked -= OnClick; // always unsubscribe
}
private void OnClick(object? sender, EventArgs e) { }
}
Delegate Invocation on Null
// Throws NullReferenceException if no handlers
MyDelegate handler = null;
handler(); // CRASH!
// Safe invocation patterns
handler?.Invoke(); // null-conditional (preferred)
handler?.DynamicInvoke(); // alternative
Weak Event Patterns
For long-lived publishers with short-lived subscribers, consider WeakEventManager (WPF) or use weak reference patterns to avoid manual unsubscription.
Key Takeaways
- Delegates are type-safe function pointers; events are restricted delegates implementing the observer pattern.
- Use
Action,Func, andPredicateinstead of custom delegate types when possible. - Lambdas and closures are powerful — be careful with captured loop variables.
- Always unsubscribe from events to prevent memory leaks.
- Follow the standard event pattern (
EventArgssubclass,protected virtual OnXxx,event EventHandler<T>) for public APIs. - Use null-conditional invocation (
event?.Invoke(...)) to avoidNullReferenceException.
Interview Questions
Q: What is a delegate? A type-safe function pointer that references a method with a matching signature. Delegates enable passing methods as parameters, callback mechanisms, and multicast invocation.
Q: What is the difference between a delegate and an event?
An event is a delegate wrapped with access restrictions. External code can only subscribe (+=) or unsubscribe (-=) from an event, but cannot invoke it or replace it. A raw delegate can be invoked and reassigned from outside.
Q: What are Action and Func?
Action is a built-in delegate for methods returning void. Func<T, TResult> is a built-in delegate for methods returning a value. Both support up to 16 type parameters.
Q: What is a closure? A closure occurs when a lambda expression captures a variable from its enclosing scope. The captured variable's lifetime is extended to match the delegate's lifetime.
Q: What is a multicast delegate? A delegate that holds references to multiple methods in an invocation list. When invoked, all methods execute sequentially. Only the last return value is returned for non-void delegates.
Q: Why should you unsubscribe from events? Event handlers hold strong references to subscribers. If a short-lived subscriber subscribes to a long-lived publisher without unsubscribing, the subscriber cannot be garbage collected, causing a memory leak.