How Multicast Delegates Work in C
In C#, a multicast delegate is a delegate that holds references to
more than one method, allowing multiple methods to be executed through a
single invocation. When delegates are chained together using the
addition assignment operator (+=), the runtime maintains an
internal invocation list to track each target method. This article
explains how multicast delegates are constructed, the sequential order
in which methods execute, how return values and exceptions are handled,
and how to safely manage invocation lists when custom behavior is
required.
The Invocation List and the Delegate Type
All delegates in modern C# implicitly derive from
System.MulticastDelegate, which itself inherits from
System.Delegate. The core mechanism behind chaining is the
_invocationList object array maintained by the
MulticastDelegate class.
When you instantiate a delegate, its invocation list initially
contains just that single method. When you use the +=
operator, the C# compiler translates this into a call to
Delegate.Combine. This method returns a new delegate
instance containing an array with references to all combined methods,
leaving the original delegates immutable. Conversely, using the
-= operator calls Delegate.Remove, which
searches the invocation list from the end and removes the last matching
method reference.
Action notify = SendEmail;
notify += LogToDatabase; // Delegate.Combine under the hood
notify(); // Both methods will be invokedExecution Order
When a multicast delegate is invoked, the runtime iterates through its internal invocation list sequentially. The methods are called synchronously in the exact order they were appended (first-in, first-out). Control passes to the first method, waits for it to complete, passes to the second method, and continues until every method in the list has executed.
Handling Return Values
While multicast delegates commonly have a void return
type, C# permits non-void multicast delegates. However, chaining methods
with return values introduces a significant limitation: the caller only
receives the return value of the last method in the invocation
list.
Every preceding method still runs, but its returned value is discarded. Because of this behavior, chaining delegates with non-void return types is generally discouraged unless paired with manual iteration.
Func<int> getNumber = () => 5;
getNumber += () => 10;
int result = getNumber(); // result is 10; 5 is evaluated but discardedException Propagation
A critical consideration when chaining methods is exception handling. If any method in the invocation list throws an unhandled exception:
- The invocation halts immediately.
- The exception bubbles up to the caller.
- Any subsequent methods remaining in the invocation list are never executed.
This behavior can leave an application in an inconsistent state, particularly when using delegates for event handling where all subscribers are expected to receive notifications.
Manual Invocation
Using GetInvocationList()
To overcome the limitations regarding discarded return values and
interrupted execution chains, you can manually iterate through the
delegate's invocation list using the GetInvocationList()
method.
GetInvocationList() returns an array of
Delegate instances representing the chain. By iterating
through this array with a foreach loop, you can wrap each
call in a try-catch block to ensure all methods run even if
one fails, or collect the return value from every target:
Func<int> getNumber = () => 5;
getNumber += () => 10;
List<int> results = new List<int>();
foreach (Func<int> handler in getNumber.GetInvocationList())
{
try
{
results.Add(handler());
}
catch (Exception ex)
{
// Handle exception for this specific method without halting the loop
}
}
// 'results' now contains both 5 and 10By leveraging Delegate.Combine, sequential invocation
lists, and defensive handling via GetInvocationList(),
multicast delegates provide the foundational architecture for the
publish-subscribe pattern and event-driven programming across the .NET
runtime.