Unlocking the power of function references, events, and runtime flexibility in C#
Delegates are the core building block of dynamic, event-driven, and functional programming in C#. They allow methods to be treated as first-class objects, stored in variables, passed as parameters, chained together, and invoked at runtime.
This guide goes beyond basic definitions. By the end, you’ll understand:
- What delegates are and how they work internally
- Single-cast vs multicast delegates
- Anonymous methods and lambda expressions
- Built-in generic delegates (Func, Action, Predicate)
- Events and the difference between delegates and events
- Delegate chains, multicast pitfalls, and invocation lists
- Real-world use cases in LINQ, callbacks, and GUI programming
- Best practices, patterns, and debugging tips
This is the definitive guide for beginners, intermediates, and even advanced developers who want to master delegates.
🔍 What is a Delegate?
At its core, a delegate is a type-safe function pointer. It defines a signature (parameter types and return type), and any method matching that signature can be assigned to it.
Think of it as a contract:
- “I accept two integers and return an integer”
- “I accept a string and return void”
public delegate int MathOperation(int a, int b);
Here, MathOperation can reference any method taking two ints and returning an int.
Why Delegates Exist
- Callbacks – Execute a method dynamically later.
- Events – Power event-driven programming in UI, servers, and libraries.
- Loose coupling – Decouple classes from concrete implementations.
- Functional patterns – Pass behavior as an argument.
- Dynamic pipelines – Methods can be selected at runtime.
⚡ Single-Cast vs Multicast Delegates
Single-Cast Delegates
A single-cast delegate references exactly one method:
public delegate void Logger(string message);
class Program
{
static void ConsoleLog(string msg) => Console.WriteLine(msg);
static void Main()
{
Logger log = ConsoleLog;
log("Hello World!"); // Calls ConsoleLog
}
}
✅ Key points:
- Only one method
- Type-safe: compiler ensures signature match
- Can reference static or instance methods
Multicast Delegates
A delegate can reference multiple methods in a chain. This is called a multicast delegate:
Logger log = ConsoleLog;
log += msg => System.IO.File.AppendAllText("log.txt", msg + "\n");
log("Logging to console and file!");
- Use
+=to add methods,-=to remove them - All methods are called in order
- Exceptions in one method stop execution unless handled
Advanced Tip: You can inspect the invocation list:
foreach (var d in log.GetInvocationList())
{
Console.WriteLine(d.Method.Name);
}
🧠 Anonymous Methods & Lambda Expressions
Anonymous Methods
Provide inline method bodies without naming them:
Logger log = delegate(string msg) { Console.WriteLine(msg); };
log("Anonymous method!");
Lambda Expressions
The modern standard, concise way to declare inline methods:
Logger log = msg => Console.WriteLine(msg);
log("Lambda expression!");
Why Lambdas Matter:
- Used extensively in LINQ
- Simplify event handlers and callbacks
- Enable functional patterns without verbose code
🛠 Generic Delegates: Func, Action, Predicate
C# provides generic delegates for most scenarios:
| Delegate | Parameters | Returns | Notes |
|---|---|---|---|
| Func<T1,…,TReturn> | N | Returns value | Replace custom delegates |
| Action<T1,…> | N | Void | No return value |
| Predicate<T> | 1 | bool | Filter/condition logic |
Examples:
Func<int,int,int> add = (a,b) => a + b;
Console.WriteLine(add(3,4)); // 7
Action<string> shout = msg => Console.WriteLine(msg.ToUpper());
shout("hello world"); // HELLO WORLD
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // True
🌐 Delegates vs Events
Delegates are method references.
Events are encapsulated delegates with restricted invocation.
public delegate void Notify(string message);
class Process
{
public event Notify OnComplete;
public void Run()
{
Console.WriteLine("Processing...");
OnComplete?.Invoke("Done!");
}
}
✅ Notes:
- Only the declaring class can invoke the event
- Prevents external misuse
- Essential for safe public APIs
🧩 Real-World Usage Patterns
Callbacks
public delegate void Callback(string message);
class Worker
{
public void DoWork(Callback callback)
{
Console.WriteLine("Working...");
callback("Work complete!");
}
}
Worker w = new Worker();
w.DoWork(msg => Console.WriteLine(msg));
Sorting With Delegates
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
names.Sort((a,b) => b.Length.CompareTo(a.Length)); // Delegate via lambda
Console.WriteLine(string.Join(", ", names)); // Charlie, Alice, Bob
Event-Driven GUI
button.Click += (s,e) => MessageBox.Show("Button clicked!");
LINQ Filtering
List<int> numbers = new List<int> { 1,2,3,4,5 };
var even = numbers.FindAll(n => n % 2 == 0); // Predicate delegate
Console.WriteLine(string.Join(",", even)); // 2,4
📦 Advanced Delegate Topics
Delegate Chaining
- Multiple delegates can be chained
- Return values: Only last method’s return value is used
Func<int,int> compute = x => x+1;
compute += x => x*2; // Last result returned
Exception Handling in Multicast
- One method throwing an exception prevents downstream calls
- Use
try-catchinside each delegate
Covariance & Contravariance
- Delegates can reference derived return types (covariance)
- Accept base types as parameters (contravariance)
Func<object> getObj = () => "string"; // Covariance
Action<string> act = obj => Console.WriteLine(obj); // Contravariance
🧰 Debugging Delegates
- Use
.GetInvocationList()to inspect chained methods - Check for null before invoking (
?.Invoke) - Multicast delegates require careful handling of exceptions
- Use logging inside delegate methods for tracing
✅ Best Practices
- Prefer Func, Action, Predicate over custom delegates
- Wrap delegate exposure with events
- Check null before invoking (
?.Invoke) - Handle exceptions inside multicast delegates
- Use lambda expressions for short inline methods
- Keep delegate chains readable
- Document delegate usage clearly
🧠 Under the Hood: How Delegates Work
- Delegates are objects storing:
- Method pointer
- Target object (for instance methods)
- Multicast delegates maintain internal invocation lists
- Invocation is type-checked at runtime
- Efficient and optimized by the runtime
📊 Section 11 — Summary Table
| Feature | Syntax | Notes |
|---|---|---|
| Single-cast | Logger log = ConsoleLog; | One method only |
| Multicast | log += FileLog; | Multiple methods |
| Anonymous | delegate(string msg){...} | Inline, no name |
| Lambda | msg => Console.WriteLine(msg) | Concise modern syntax |
| Func / Action / Predicate | Func<int,int,int> add = ... | Generic built-ins |
| Event | public event Notify OnComplete; | Safe public exposure |
| User-defined | public delegate string Formatter(string input); | Semantic clarity |
Final Thoughts
Delegates are the cornerstone of dynamic, functional, and event-driven programming in C#.
Once mastered, they enable:
- Callbacks, events, and LINQ patterns
- Decoupled, reusable, and flexible code
- Runtime behavior selection
- Clean integration with modern C# features
Delegates make C# more than object-oriented — they bring methods to life as first-class citizens, empowering developers to build robust, maintainable, and elegant solutions.