Delegate Fundamentals are the foundation for treating methods as first-class objects in C#. At its core, a delegate is a type-safe function pointer; it defines a signature (return type and parameters) that compatible methods can be assigned to. The core purpose is to enable decoupled communication between components.
Multicast
A multicast delegate is a delegate that can hold references to more than one method. When invoked, it calls each method in its invocation list in order. This is the underlying mechanism for events in C#.
// 1. Define a delegate with a void return type (suitable for multicast)
public delegate void LogMessage(string message);
class Program
{
static void LogToConsole(string msg)
{
Console.WriteLine($"Console: {msg}");
}
static void LogToFile(string msg)
{
// Simulate file logging
System.IO.File.AppendAllText("log.txt", $"File: {msg}\n");
}
static void Main()
{
// 2. Create a delegate instance and add multiple methods using +=
LogMessage logger = LogToConsole;
logger += LogToFile; // Multicast: Adds LogToFile to the invocation list
logger += LogToConsole; // Can even add the same method twice
// 3. Invoking 'logger' calls all methods in the list
logger("Application started!");
// Output:
// Console: Application started!
// Console: Application started!
// (And writes to "log.txt")
// 4. Remove a method from the list
logger -= LogToFile;
logger("File logger removed.");
}
}
Exception Handling: If a method in a multicast delegate’s invocation list throws an exception, subsequent methods are not invoked. The exception propagates up immediately.
Covariance
Covariance with delegates allows a method to return a more derived type than what is specified by the delegate’s return type. This provides flexibility when assigning methods to delegates, aligning with the Liskov Substitution Principle.
// Base and derived classes
class Animal { public string Name = "Animal"; }
class Giraffe : Animal { public string Name = "Giraffe"; }
// 1. Delegate defined to return a base type 'Animal'
public delegate Animal AnimalFactory();
class Program
{
// 2. A method that returns the more derived type 'Giraffe'
static Giraffe CreateGiraffe()
{
return new Giraffe();
}
static void Main()
{
// 3. Covariance in action: Assigning a method returning 'Giraffe'
// to a delegate expecting a return type of 'Animal'. This is allowed.
AnimalFactory factory = CreateGiraffe;
// 4. Invoke the delegate. The return type is 'Animal', but the actual object is a 'Giraffe'.
Animal animal = factory();
Console.WriteLine(animal.Name); // Output: Giraffe
}
}
Contravariance: While covariance applies to return types, contravariance applies to parameters. A delegate can point to a method that accepts a less derived (more general) parameter type.
Action/Func
Action and Func are generic delegate types provided by the .NET Framework, eliminating the need to manually define custom delegate types for most scenarios.
- Action: Represents a method that does not return a value (
void)
- Func: Represents a method that returns a value
using System;
class Program
{
static void Main()
{
// --- Using Action\<T\> for void methods ---
// Action<string> is equivalent to: delegate void SomeDelegate(string message);
Action<string> consoleLogger = (message) => Console.WriteLine($"LOG: {message}");
consoleLogger("Hello Action!"); // Invokes the lambda expression
// Action can have multiple parameters: Action<T1, T2>
Action<string, int> repeatLogger = (msg, count) =>
{
for (int i = 0; i < count; i++) Console.WriteLine(msg);
};
repeatLogger("Repeat!", 3);
// --- Using Func<T, TResult> for methods with return values ---
// Func<int, int, string> is equivalent to: delegate string SomeDelegate(int a, int b);
Func<int, int, string> sumFormatter = (a, b) => $"The sum is {a + b}";
string result = sumFormatter(5, 3);
Console.WriteLine(result); // Output: The sum is 8
// Func with no input parameters, just a return type: Func<string>
Func<DateTime> getCurrentTime = () => DateTime.Now;
Console.WriteLine($"Current time: {getCurrentTime()}");
}
}
Modern C# Practice: Use Action and Func instead of defining custom delegates unless you need specific naming for clarity.
Why Delegate Fundamentals are Important
- Strategy Pattern (Open/Closed Principle): Delegates allow you to inject specific algorithms or behaviors into a class
- Event-Driven Architecture (Loose Coupling): They are the bedrock of the .NET eventing system
- Powering LINQ and Functional Programming: Delegates enable LINQ queries and functional programming concepts
Advanced Nuances
Safe Multicast Invocation
logger += (s) => throw new InvalidOperationException("Demo exception!");
logger += LogToConsole; // This method will never be called if the previous one throws.
// Safe invocation
foreach (LogMessage handler in logger.GetInvocationList())
{
try { handler("test"); }
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
Delegate Inference and Method Group Conversion
The compiler can often infer the delegate type, allowing for very concise syntax:
// Instead of: button.Click += new EventHandler(OnButtonClick);
button.Click += OnButtonClick; // Method group conversion
Roadmap Context
Delegate Fundamentals are the absolute prerequisite for the entire “Delegates and Events” section. This foundational knowledge unlocks:
- Events: Built on top of multicast delegates with encapsulation
- Lambda Expressions & Anonymous Methods: Shorthand ways to create delegate instances
- Functional Programming Patterns: Higher-order functions implemented using delegates
- Asynchronous Programming:
async and await rely heavily on delegates under the hood