
Learn C# delegates and events with practical, runnable code examples. Master event-driven programming, best practices, and common pitfalls. Start coding today!
If you've ever wondered how a button "knows" when it's clicked, or how one part of your application can notify another without being tightly coupled to it, the answer lies in C# delegates and events. Understanding delegates and events is one of the most important steps in moving from beginner to professional .NET developer, because they power everything from UI frameworks and async callbacks to the publish-subscribe patterns used in enterprise software. In this tutorial, we'll explain C# delegates and events from the ground up, with practical, runnable code examples and a complete event-driven mini-application.
By the end of this guide you'll know exactly what a delegate is, how it relates to events, when to use built-in types like Action and Func, and the best practices that keep your event-driven code safe and maintainable.
What Are Delegates in C#?
A delegate in C# is a type that holds a reference to a method. Think of it as a strongly-typed function pointer: it defines a method signature (return type and parameters), and any method matching that signature can be assigned to it. This lets you pass methods around as arguments, store them in variables, and invoke them dynamically at runtime.
Why does this matter? Delegates enable decoupling. Instead of calling a method directly, you call it through a delegate, which means the caller doesn't need to know which concrete method will run. This is the foundation of event-driven programming in C#.
using System;
public class Program
{
// 1. Declare a delegate type that takes a string and returns void
public delegate void Notify(string message);
public static void Main()
{
// 2. Point the delegate at a real method
Notify notifier = SendEmail;
// 3. Invoke the method through the delegate
notifier("Order #1024 has shipped!");
// You can reassign it to a different method with the same signature
notifier = SendSms;
notifier("Order #1024 has shipped!");
}
static void SendEmail(string message) => Console.WriteLine($"EMAIL: {message}");
static void SendSms(string message) => Console.WriteLine($"SMS: {message}");
}
Notice how notifier can call SendEmail or SendSms without the Main method caring which one runs. That flexibility is the whole point of a delegate.
Multicast Delegates: Calling Many Methods at Once
One powerful feature of delegates is that they are multicast, meaning a single delegate instance can reference multiple methods. Use the += operator to add methods and -= to remove them. When invoked, all subscribed methods run in the order they were added.
Notify notifier = SendEmail;
notifier += SendSms; // now both run
notifier += LogToConsole;
notifier("Payment received");
// Output:
// EMAIL: Payment received
// SMS: Payment received
// LOG: Payment received
This multicast behavior is exactly what makes the C# event model work — a single notification can reach many listeners.
Action, Func, and Predicate: Built-In Delegates
In modern C# you rarely need to declare your own delegate types. The .NET base class library provides generic built-in delegates that cover almost every scenario, and using them makes your code more idiomatic and readable:
- Action — references a method that returns
void.Action<string>takes a string and returns nothing. - Func — references a method that returns a value. The last generic argument is the return type, e.g.
Func<int, int, int>takes two ints and returns an int. - Predicate — references a method that returns a
bool, typically used for filtering, e.g.Predicate<int>.
Action<string> log = msg => Console.WriteLine(msg);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;
log("Using built-in delegates");
Console.WriteLine(add(3, 4)); // 7
Console.WriteLine(isEven(10)); // True
The lambda expressions (=>) above are simply concise, inline methods assigned to the delegates. If you've used LINQ, you've already been using delegates without realizing it.
What Are Events in C#?
An event in C# is a special wrapper around a delegate that enforces the publish-subscribe pattern. It allows a class (the publisher) to notify other classes (the subscribers) when something happens, while protecting the underlying delegate from misuse.
The key difference between a plain delegate and an event: with a public delegate field, any outside code could overwrite all subscribers with =, or even invoke the delegate itself. An event restricts outside code to only += (subscribe) and -= (unsubscribe). Only the declaring class can raise (invoke) the event. This encapsulation is why events are the recommended way to implement notifications in C#.
A Practical Event-Driven Example in C#
Let's build something realistic: a stock price monitor. When a stock's price changes, the system should automatically notify multiple subscribers — a dashboard, an alert service, and a logger — without the stock class knowing anything about them. This is event-driven programming in C# in action.
The .NET convention is to use the EventHandler<TEventArgs> delegate and a custom EventArgs class to carry event data.
using System;
// 1. Custom event data, by convention inherits from EventArgs
public class PriceChangedEventArgs : EventArgs
{
public string Symbol { get; }
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public PriceChangedEventArgs(string symbol, decimal oldPrice, decimal newPrice)
{
Symbol = symbol;
OldPrice = oldPrice;
NewPrice = newPrice;
}
}
// 2. The publisher
public class Stock
{
private decimal _price;
public string Symbol { get; }
// The event, based on the generic EventHandler delegate
public event EventHandler<PriceChangedEventArgs> PriceChanged;
public Stock(string symbol, decimal price)
{
Symbol = symbol;
_price = price;
}
public decimal Price
{
get => _price;
set
{
if (_price == value) return;
var oldPrice = _price;
_price = value;
// Raise the event safely
OnPriceChanged(new PriceChangedEventArgs(Symbol, oldPrice, value));
}
}
// 3. Protected virtual method to raise the event (the standard pattern)
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
// Null-conditional operator prevents a crash if there are no subscribers
PriceChanged?.Invoke(this, e);
}
}
Now the subscribers. Each one reacts to the same event independently:
public class Program
{
public static void Main()
{
var apple = new Stock("AAPL", 190.00m);
// Subscribe multiple handlers to the same event
apple.PriceChanged += Dashboard_Update;
apple.PriceChanged += AlertService_Check;
apple.PriceChanged += (sender, e) =>
Console.WriteLine($"LOG: {e.Symbol} moved {e.OldPrice} -> {e.NewPrice}");
// Changing the price automatically notifies every subscriber
apple.Price = 195.50m;
apple.Price = 188.25m;
// Unsubscribe when no longer needed
apple.PriceChanged -= Dashboard_Update;
}
static void Dashboard_Update(object sender, PriceChangedEventArgs e)
=> Console.WriteLine($"DASHBOARD: {e.Symbol} is now ${e.NewPrice}");
static void AlertService_Check(object sender, PriceChangedEventArgs e)
{
var change = (e.NewPrice - e.OldPrice) / e.OldPrice * 100;
if (Math.Abs(change) > 2)
Console.WriteLine($"ALERT: {e.Symbol} moved {change:F2}%!");
}
}
Run this and a single apple.Price = 195.50m assignment triggers the dashboard, the alert service, and the logger — all without the Stock class containing a single reference to any of them. That is the power of events: loose coupling. You can add or remove subscribers at any time without touching the publisher.
Delegates vs Events: What's the Difference?
This is one of the most common interview questions about C# delegates and events, so let's make it crystal clear:
- A delegate is a type that holds method references. It can be invoked and reassigned by anyone who can access it.
- An event is a layer of encapsulation built on top of a delegate. Outside code can only subscribe (
+=) or unsubscribe (-=); only the owning class can raise it.
In short: every event uses a delegate under the hood, but not every delegate is an event. Use a plain delegate when you need to pass a method as a parameter (like a callback or a LINQ filter). Use an event when you're modeling notifications that external code should react to but not control.
Best Practices for C# Delegates and Events
- Always use the null-conditional operator when raising events:
PriceChanged?.Invoke(this, e). This avoids aNullReferenceExceptionwhen there are no subscribers. - Follow the standard event pattern: use
EventHandler<TEventArgs>, a customEventArgssubclass, and aprotected virtual OnXxxmethod so derived classes can override behavior. - Prefer Action and Func over custom delegate types for callbacks. They're instantly recognizable to other developers.
- Unsubscribe to prevent memory leaks. A subscriber that never unsubscribes keeps the publisher alive, and vice versa — a frequent cause of leaks in long-lived applications.
- Keep event handlers fast and exception-safe. Because handlers run synchronously in sequence, one slow or throwing handler can block or break the rest.
Common Pitfalls to Avoid
- Exceptions in a handler stop the chain. If the second of three subscribers throws, the third never runs. For critical scenarios, invoke handlers individually using
GetInvocationList()and wrap each in a try-catch. - The lambda unsubscribe trap. You cannot unsubscribe an anonymous lambda with
-=because you don't hold a reference to it. Store it in a variable first if you'll need to detach it. - Race conditions in multithreaded code. Copy the event to a local variable before checking and invoking, or rely on the null-conditional operator which does this for you.
- Forgetting that delegates are immutable.
+=and-=create new delegate instances rather than mutating the existing one — important to understand when reasoning about threading.
Conclusion: Key Takeaways
Mastering C# delegates and events unlocks a whole category of clean, decoupled, event-driven designs that scale well as your applications grow. Here are the key takeaways:
- A delegate is a strongly-typed reference to a method, enabling you to pass behavior around as data.
- Multicast delegates let one call invoke many methods, which is the engine behind events.
- Use built-in
Action,Func, andPredicateinstead of declaring custom delegate types in most cases. - An event is an encapsulated delegate that safely implements the publish-subscribe pattern — the standard way to do notifications in .NET.
- Follow the
EventHandler<T>+EventArgs+OnXxxpattern, always null-check before raising, and unsubscribe to avoid memory leaks.
Now that you understand how delegates and events work together, try extending the stock monitor example: add a handler that logs to a file, or build your own event-driven notification system. Hands-on practice is the fastest way to make event-driven programming in C# second nature. Happy coding!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment