Delegates and events are fundamental to event-driven programming in C#. Understanding delegate semantics, variance, and proper event patterns is essential for building robust, decoupled systems.
Delegates are type-safe function pointers. Multicast delegates hold invocation lists. Covariance allows return type widening; contravariance allows parameter type widening in delegate assignment.
A delegate in C# is an object that holds a method reference — strongly typed unlike function pointers. Multicast delegates chain multiple methods; invoking the delegate calls all.
// Covariance: return type narrowsFunc<Animal> factory = () => new Dog(); // Dog : Animal ✓// Contravariance: parameter type widensAction<Dog> handler = (Animal a) => Log(a); // widens ✓// Multicast: all methods invokedAction<string> log = Console.WriteLine;log += s => File.AppendAllText("log.txt", s);log += s => Telemetry.Track(s);log("hello"); // 3 methods called in order// Unsubscribe: -= removes first matching entrylog -= Console.WriteLine;
Capture a local copy of a multicast delegate before invoking it to make the invocation thread-safe: var handler = _event; handler?.Invoke() — this prevents race conditions if subscribers remove themselves during invocation.
Action delegates represent methods that return void.
// No parametersAction notify = () => Console.WriteLine("Notification");notify();// With parameters (up to 16)Action<string, int> log = (msg, level) => Console.WriteLine($"[{level}] {msg}");log("Error occurred", 3);
Use Action when you need to execute code without returning a value.
Func delegates represent methods that return a value.
// No parametersFunc<int> getCount = () => 42;int count = getCount();// With parameters (up to 16 input, 1 output)Func<int, int, int> add = (a, b) => a + b;int sum = add(5, 3);// Used in LINQvar evens = numbers.Where(n => n % 2 == 0);
Use Func when you need to compute and return a value.
Predicate delegates test a condition and return bool.
// Predicate<T> is equivalent to Func<T, bool>Predicate<int> isEven = n => n % 2 == 0;var numbers = new List<int> { 1, 2, 3, 4, 5 };var evenNumbers = numbers.FindAll(isEven);// Same as:Func<int, bool> isEvenFunc = n => n % 2 == 0;var evenFiltered = numbers.Where(isEvenFunc).ToList();
Use Predicate with collection methods like FindAll, Exists, TrueForAll.
// Multicast return value behaviorFunc<int> multiFunc = () => 1;multiFunc += () => 2;multiFunc += () => 3;int result = multiFunc(); // result = 3 (last return value wins)// Exception handling in multicastAction multiAction = () => Console.WriteLine("First");multiAction += () => throw new Exception("Second failed");multiAction += () => Console.WriteLine("Third"); // Never executestry{ multiAction(); // First executes, then exception thrown, Third skipped}catch (Exception ex){ Console.WriteLine($"Caught: {ex.Message}");}
When a multicast delegate throws an exception, remaining delegates in the invocation list are not called. For critical scenarios, invoke each delegate manually in a try-catch.
Events are multicast delegate fields with restricted access — only the declaring class can invoke them. Custom event accessors enable thread-safe subscription and custom storage.
The event keyword wraps a delegate with restricted access: only the declaring class invokes; subscribers use += and -=. Custom event accessors (add/remove) enable lock-based thread safety, weak event patterns, and custom subscriber storage.
// Standard event pattern with EventHandler<TEventArgs>public class OrderProcessor{ // Event declaration public event EventHandler<OrderEventArgs>? OrderPlaced; public event EventHandler<OrderEventArgs>? OrderCancelled; // Protected virtual method to raise event protected virtual void OnOrderPlaced(Order order) { OrderPlaced?.Invoke(this, new OrderEventArgs(order)); } protected virtual void OnOrderCancelled(Order order) { OrderCancelled?.Invoke(this, new OrderEventArgs(order)); } public void PlaceOrder(Order order) { // Process order OnOrderPlaced(order); }}// EventArgs derived classpublic class OrderEventArgs : EventArgs{ public Order Order { get; } public DateTime Timestamp { get; } public OrderEventArgs(Order order) { Order = order; Timestamp = DateTime.UtcNow; }}
Always copy the delegate to a local variable before null-checking and invoking in multi-threaded code — otherwise a subscriber could unsubscribe between your null check and your Invoke() call.
var processor = new OrderProcessor();// Subscribe with lambdaprocessor.OrderPlaced += (sender, e) =>{ Console.WriteLine($"Order placed: {e.Order.Id}");};// Subscribe with methodprocessor.OrderPlaced += OnOrderPlaced;void OnOrderPlaced(object? sender, OrderEventArgs e){ // Handle event}// Unsubscribeprocessor.OrderPlaced -= OnOrderPlaced;
// Weak event pattern prevents memory leakspublic class WeakEventManager<TEventArgs> where TEventArgs : EventArgs{ private readonly List<WeakReference<EventHandler<TEventArgs>>> _handlers = new(); public void AddHandler(EventHandler<TEventArgs> handler) { _handlers.Add(new WeakReference<EventHandler<TEventArgs>>(handler)); } public void RemoveHandler(EventHandler<TEventArgs> handler) { _handlers.RemoveAll(wr => { if (!wr.TryGetTarget(out var target)) return true; return target == handler; }); } public void Raise(object? sender, TEventArgs args) { _handlers.RemoveAll(wr => !wr.TryGetTarget(out _)); foreach (var weakRef in _handlers.ToList()) { if (weakRef.TryGetTarget(out var handler)) handler(sender, args); } }}
// Async event patternpublic class AsyncEventArgs : EventArgs{ public Task? CompletionTask { get; set; }}public class Publisher{ public event EventHandler<AsyncEventArgs>? DataReceived; protected virtual async Task OnDataReceivedAsync() { var handler = DataReceived; if (handler != null) { var args = new AsyncEventArgs(); var tasks = new List<Task>(); foreach (EventHandler<AsyncEventArgs> del in handler.GetInvocationList()) { var task = Task.Run(() => del(this, args)); tasks.Add(task); if (args.CompletionTask != null) tasks.Add(args.CompletionTask); } await Task.WhenAll(tasks); } }}
// Common memory leak patternpublic class Subscriber : IDisposable{ private readonly Publisher _publisher; public Subscriber(Publisher publisher) { _publisher = publisher; _publisher.DataReceived += OnDataReceived; // Creates strong reference } private void OnDataReceived(object? sender, EventArgs e) { // Handle event } public void Dispose() { // MUST unsubscribe to prevent leak _publisher.DataReceived -= OnDataReceived; }}
Always unsubscribe from events when the subscriber is disposed. Event subscriptions create strong references from publisher to subscriber, preventing garbage collection.
// Inefficient: creates new delegate instance on each callfor (int i = 0; i < 1000; i++){ items.ForEach(item => Process(item)); // Allocates delegate}// Efficient: cache the delegateprivate static readonly Action<Item> ProcessAction = item => Process(item);for (int i = 0; i < 1000; i++){ items.ForEach(ProcessAction); // Reuses cached delegate}// Or use a method group (also cached by compiler)for (int i = 0; i < 1000; i++){ items.ForEach(Process); // Method group conversion cached}
Caching delegates avoids allocation overhead in hot paths.