C# Delegates & Events Cheat Sheet
Covers delegates, multicast delegates, the built-in Func, Action, and Predicate types, and the C# event pattern for publish-subscribe notifications.
Delegates
Type-safe references to methods, including multicast.
public delegate int MathOperation(int a, int b); // Declare a delegate typeint Add(int a, int b) => a + b;int Multiply(int a, int b) => a * b;MathOperation op = Add;Console.WriteLine(op(3, 4)); // 7op = Multiply;Console.WriteLine(op(3, 4)); // 12// Multicast delegateAction<string> logger = Console.WriteLine;logger += msg => File.AppendAllText("log.txt", msg);logger("Hello"); // Invokes both handlers in order
Func, Action, Predicate
Built-in generic delegate types for common shapes.
Func<int, int, int> add = (a, b) => a + b; // Takes params, returns a valueAction<string> print = msg => Console.WriteLine(msg); // Takes params, returns voidPredicate<int> isEven = n => n % 2 == 0; // Takes a param, returns boolint sum = add(2, 3);print("done");bool result = isEven(4);
Events
The standard publish-subscribe pattern.
public class Button{ public event EventHandler<EventArgs>? Clicked; // Standard event pattern public void SimulateClick() { Clicked?.Invoke(this, EventArgs.Empty); // Null-safe raise }}var button = new Button();button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");button.SimulateClick();
Key Concepts
Terminology for delegates and events.
- delegate- A type-safe reference to one or more methods with a matching signature
- Multicast delegate- Combining delegates with += invokes each one in sequence when called
- event keyword- Restricts external code to += / -= only, preventing direct invocation or overwrite
- EventHandler / EventHandler<T>- Standard delegate signature: (object? sender, EventArgs e)
- Func<T,...,TResult>- Built-in generic delegate for methods that return a value
- Action<T,...>- Built-in generic delegate for methods that return void
- Null-conditional invoke- handler?.Invoke(...) avoids a NullReferenceException when there are no subscribers
Custom EventArgs & Unsubscribing
Passing typed data with events and preventing memory leaks by detaching handlers.
public class OrderPlacedEventArgs : EventArgs{ public int OrderId { get; } public decimal Total { get; } public OrderPlacedEventArgs(int orderId, decimal total) => (OrderId, Total) = (orderId, total);}public class OrderProcessor{ public event EventHandler<OrderPlacedEventArgs>? OrderPlaced; public void Place(int id, decimal total) => OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(id, total));}var processor = new OrderProcessor();EventHandler<OrderPlacedEventArgs> handler = (s, e) => Console.WriteLine($"Order {e.OrderId}: {e.Total:C}");processor.OrderPlaced += handler;// ... later, before the subscriber's lifetime ends:processor.OrderPlaced -= handler; // Prevents the publisher from keeping the subscriber alive
GetInvocationList & Manual Combine/Remove
Inspecting and composing multicast delegates without the += / -= sugar.
Action a1 = () => Console.WriteLine("first");Action a2 = () => Console.WriteLine("second");Delegate combined = Delegate.Combine(a1, a2);foreach (Action d in combined.GetInvocationList()) d(); // Invoke each subscriber individually, e.g. to isolate exceptions// Aggregating return values from a multicast Func requires GetInvocationList tooFunc<int> f1 = () => 1;Func<int> f2 = () => 2;Func<int> both = f1 + f2;int lastResult = both(); // Only 2 — invoking directly discards all but the last resultint[] allResults = both.GetInvocationList().Select(d => ((Func<int>)d)()).ToArray(); // [1, 2]
Delegate Covariance & Contravariance
Assignment compatibility for delegates with related, non-identical signatures.
// Covariant return type: a delegate expecting object can be satisfied by a method returning stringFunc<object> getObj = () => "hello";// Contravariant parameter type: a delegate expecting string can be satisfied by a method accepting objectAction<string> printStr = (object o) => Console.WriteLine(o);// Common real-world case: EventHandler<DerivedEventArgs> can be assigned where EventHandler<EventArgs> is expectedvoid Handler(object? sender, EventArgs e) => Console.WriteLine("handled");EventHandler<FileSystemEventArgs> fsHandler = Handler; // object/EventArgs params are contravariant-compatible
Closure Capture in Loops
A classic bug where all subscribed handlers observe the final loop value.
var buttons = new List<Button>();for (int i = 0; i < 3; i++){ int captured = i; // Copy into a loop-scoped local buttons[i].Clicked += (s, e) => Console.WriteLine($"Button {captured} clicked"); // Without the copy, C# >= 5 for-loops still create a new 'i' per iteration for 'for', // but 'foreach' variables and mutable outer locals remain a common source of this bug // in older code or when the loop variable is reused across nested delegates.}
Advanced Delegate & Event Concepts
Terminology for production-grade delegate and event usage.
- Delegate equality- Two delegate instances are equal if they reference the same target and method, enabling reliable -= removal
- Thread-safety of events- += / -= are not atomic on custom event accessors; use Interlocked.CompareExchange or lock for high-concurrency publishers
- Weak event pattern- Uses WeakReference-based subscriptions so a long-lived publisher doesn't keep short-lived subscribers alive
- Custom event accessors- add { } / remove { } blocks let you customize storage, e.g. backing a large set of events with a single EventHandlerList
- Async void handlers- Event handler signatures are async void by necessity; exceptions thrown inside them cannot be awaited or caught by the caller
- Delegate as function pointer- delegate*<int,int,int> unmanaged function pointers avoid delegate allocation in hot paths (unsafe context)
- EventAggregator / Mediator- A central pub-sub hub used to decouple publishers and subscribers that shouldn't reference each other directly
Always raise events with the null-conditional operator (Clicked?.Invoke(...)) instead of a separate null check — it's safe against a subscriber unsubscribing between the check and the call.