
Learn C# delegates, events, and Func/Action with practical examples. Master event-driven programming in C# the right way — 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 is C# delegates. Delegates, events, and the built-in Func and Action types form the backbone of event-driven programming in .NET. They power everything from UI frameworks like WPF and ASP.NET Core to LINQ, async callbacks, and the observer pattern.
In this complete guide, you'll learn exactly what C# delegates are, how they relate to events and the Func/Action family, and—most importantly—why they exist. We'll cover practical, runnable code examples, best practices, and the common pitfalls that trip up beginners and intermediates alike. Whether you're searching for a simple C# delegate example or trying to understand delegate vs event, this article has you covered.
What Are C# Delegates? (The Foundation)
A delegate is a type-safe function pointer. Put simply, it's a variable that holds a reference to a method instead of holding data. Just as an int can hold a number, a delegate can hold a method—and you can pass that method around, store it, and invoke it later.
Why does this matter? Because it lets you treat behavior as data. Instead of hard-coding what happens next, you can pass in which method to call, enabling flexible, decoupled, and reusable code. This is the core idea behind event-driven programming in C#.
Here's a basic C# delegate example:
// 1. Declare a delegate type — defines the method signature it can hold
public delegate int MathOperation(int a, int b);
public class Calculator
{
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
public static void Main()
{
// 2. Assign a method to the delegate
MathOperation op = Add;
Console.WriteLine(op(3, 4)); // Output: 7
// 3. Point it at a different method — behavior changes, code doesn't
op = Multiply;
Console.WriteLine(op(3, 4)); // Output: 12
}
}
Notice how op holds different methods at different times. The calling code—op(3, 4)—never changes, but the behavior does. That's the power of delegates.
Multicast Delegates: Calling Many Methods at Once
Delegates in C# are multicast, meaning a single delegate instance can reference multiple methods. You combine them with += and remove them with -=. When invoked, all attached methods run in order.
public delegate void Notify(string message);
public class Logger
{
public static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
public static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
public static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile; // Now both methods are attached
notifier("System started");
// Output:
// Console: System started
// File: System started
}
}
This multicast behavior is exactly what makes the publish-subscribe pattern—and therefore events—possible.
Func and Action: The Built-In Delegates You Should Actually Use
In modern C#, you rarely need to declare your own delegate types. The .NET framework provides two generic delegate families that cover almost every case: Func and Action in C#.
- Action — represents a method that returns
void.Action<T>takes parameters but returns nothing. - Func — represents a method that returns a value. The last type parameter is always the return type. So
Func<int, int, int>takes two ints and returns an int. - Predicate — a specialized
Func<T, bool>used for tests and filtering.
// Action: takes input, returns nothing
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // Hello, Alice!
// Func: takes two ints, returns an int (last param is the return type)
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(5, 7)); // 12
// Predicate: returns bool — perfect for filtering
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(10)); // True
Why prefer Func and Action over custom delegates? Because they're instantly recognizable to every C# developer, they reduce boilerplate, and they integrate seamlessly with LINQ and the entire .NET ecosystem. When you call list.Where(x => x > 5), you're passing a Func<int, bool> behind the scenes.
Lambda Expressions and Anonymous Methods
The => syntax above is a lambda expression—a concise, inline way to define a method without naming it. Before lambdas, C# used anonymous methods with the delegate keyword. You'll still see both in older codebases:
// Modern lambda (preferred)
Func<int, int> square = x => x * x;
// Older anonymous method syntax (equivalent)
Func<int, int> squareOld = delegate (int x) { return x * x; };
Lambdas can also capture variables from their surrounding scope—a feature called a closure. This is powerful but also a common source of bugs, which we'll discuss in the pitfalls section.
C# Events: Delegates with Guardrails
An event is a delegate with restricted access. It's the standard mechanism for the publisher-subscriber (observer) pattern: one class (the publisher) raises a notification, and any number of other classes (subscribers) react to it—without the publisher knowing or caring who's listening.
So what's the real delegate vs event difference? A plain public delegate can be reassigned (=), invoked, or even cleared by anyone holding a reference. An event only allows external code to subscribe (+=) and unsubscribe (-=). Only the declaring class can raise (invoke) it. This encapsulation prevents subscribers from accidentally wiping out each other's handlers or firing the event themselves.
Here's the standard .NET event pattern using EventHandler<T>:
// Custom event data
public class OrderPlacedEventArgs : EventArgs
{
public string ProductName { get; }
public decimal Amount { get; }
public OrderPlacedEventArgs(string productName, decimal amount)
{
ProductName = productName;
Amount = amount;
}
}
// The publisher
public class OrderService
{
// Declare the event using the recommended EventHandler delegate
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
public void PlaceOrder(string product, decimal amount)
{
Console.WriteLine($"Processing order for {product}...");
// Raise the event safely with the null-conditional operator
OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(product, amount));
}
}
// The subscribers
public class Program
{
public static void Main()
{
var service = new OrderService();
// Subscribe: send a confirmation email
service.OrderPlaced += (sender, e) =>
Console.WriteLine($"Email sent: Thanks for buying {e.ProductName}!");
// Subscribe: update inventory
service.OrderPlaced += (sender, e) =>
Console.WriteLine($"Inventory updated for {e.ProductName} (${e.Amount})");
service.PlaceOrder("Mechanical Keyboard", 89.99m);
}
}
When PlaceOrder runs, both subscribers fire automatically. The OrderService has zero knowledge of email or inventory logic. You could add SMS notifications, analytics, or logging later—without touching OrderService at all. This loose coupling is why event-driven programming in C# scales so well in large applications.
Why Use EventHandler<T> and ?.Invoke?
Two best practices appear in the code above:
EventHandler<T>follows the .NET convention of(object sender, TEventArgs e). Tooling, documentation, and other developers all expect this signature—stick to it.OrderPlaced?.Invoke(...)uses the null-conditional operator. If no one has subscribed, the event isnull, and invoking it directly would throw aNullReferenceException. The?.check is the safest, thread-conscious way to raise an event.
Practical Use Cases for Delegates and Events
Understanding the theory is one thing; knowing when to reach for these tools is another. Here are the most common real-world scenarios:
- Callbacks: Pass an
Actionto run after an async operation completes. - LINQ queries:
Where,Select, andOrderByall takeFuncdelegates. - UI events: Button clicks, text changes, and window resizing in WPF/WinForms.
- Strategy pattern: Inject different algorithms at runtime via a delegate parameter.
- Decoupled notifications: Domain events in clean architecture and DDD.
Here's a strategy-pattern example showing how a delegate parameter makes a method endlessly flexible:
public class DataProcessor
{
// Accept ANY transformation as a Func parameter
public List<int> Process(List<int> data, Func<int, int> transform)
{
var result = new List<int>();
foreach (var item in data)
result.Add(transform(item));
return result;
}
}
// Usage — one method, infinite behaviors
var processor = new DataProcessor();
var numbers = new List<int> { 1, 2, 3, 4 };
var doubled = processor.Process(numbers, x => x * 2); // 2, 4, 6, 8
var squared = processor.Process(numbers, x => x * x); // 1, 4, 9, 16
Common Pitfalls and Best Practices
Delegates and events are elegant, but a few traps catch even experienced developers. Here are the ones worth memorizing.
1. Memory Leaks from Forgotten Unsubscriptions
This is the single most common delegate-related bug. When a subscriber subscribes to an event, the publisher holds a reference to it. If the subscriber is never unsubscribed, the garbage collector can't reclaim it—causing a memory leak that grows over the application's lifetime.
// ALWAYS unsubscribe when done, especially for long-lived publishers
service.OrderPlaced += handler;
// ... later, when the subscriber is no longer needed:
service.OrderPlaced -= handler;
Important: you can only unsubscribe a handler if you keep a reference to it. Anonymous lambdas can't be removed because you don't have a handle to them. Store the lambda in a variable if you'll need to detach it later.
2. The Closure Loop Variable Trap
When you capture a loop variable in a lambda, classic C# closures captured the variable, not its value. (C# 5+ fixed this for foreach, but the gotcha still appears in for loops.)
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i; // Capture a local copy to avoid the trap
actions.Add(() => Console.WriteLine(copy));
}
actions.ForEach(a => a()); // Prints 0, 1, 2 (not 3, 3, 3)
3. Always Null-Check Before Raising Events
As shown earlier, use MyEvent?.Invoke(this, args). Never invoke an event without the null-conditional operator unless you've guaranteed at least one subscriber.
4. Don't Over-Engineer with Custom Delegates
If Func, Action, or EventHandler<T> fits, use it. Reserve custom delegate declarations for cases where a descriptive name genuinely improves readability or when you need ref/out parameters (which Func/Action don't support).
5. Beware Exceptions in Multicast Invocations
If one subscriber in a multicast delegate throws, the remaining handlers won't run. For critical notifications, consider invoking handlers individually via GetInvocationList() and wrapping each in a try-catch.
Conclusion: Key Takeaways on C# Delegates and Events
Mastering C# delegates, events, and the Func/Action family is a milestone that separates beginner C# developers from confident, professional ones. These features are the foundation of event-driven programming in C#, LINQ, async callbacks, and clean, decoupled architecture.
Here's what to remember:
- Delegates are type-safe references to methods—they let you treat behavior as data.
- Func and Action are the built-in delegates you should use 95% of the time.
Funcreturns a value;Actionreturns void. - Events are delegates with encapsulation—they enable the publisher-subscriber pattern safely. This is the heart of the delegate vs event distinction.
- Always raise events with
?.Invoke(), follow theEventHandler<T>convention, and unsubscribe to prevent memory leaks. - Watch out for closure traps and exceptions in multicast delegates.
The best way to internalize these concepts is to use them. Try refactoring a tightly coupled class in your own project to raise an event instead, or replace a hard-coded method call with a Func parameter. Once event-driven thinking clicks, you'll write more flexible, testable, and maintainable C# code. 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