C# Delegates: Type-Safe Function Pointers Explained
This article explores delegates in C#, explaining their fundamental role as managed, type-safe function pointers within the .NET ecosystem. You will learn what delegates are, how they contrast with traditional C/C++ function pointers by enforcing strict compile-time signature checks, and how to define and execute them efficiently in your applications.
What Is a Delegate in C#?
A delegate in C# is a reference type that encapsulates a method with a specific signature and return type. Rather than holding data, a delegate holds a reference to one or more methods. Once instantiated, a delegate can invoke its referenced method as if it were called directly, allowing methods to be passed as arguments, stored in variables, and executed dynamically.
How Delegates Act as Function Pointers
In unmanaged languages like C and C++, function pointers store raw memory addresses pointing to executable code. While powerful, traditional function pointers are inherently unsafe: they do not guarantee that the target function matches the expected parameters or return type, which can lead to memory corruption, stack imbalance, and runtime crashes.
A C# delegate provides the same core capability—decoupling the caller
from the target method—while executing under the control of the Common
Language Runtime (CLR). Instead of pointing to arbitrary memory
locations, a delegate is an object derived from
System.MulticastDelegate. It maintains metadata about the
method it targets, including a reference to the method and the object
instance on which the method should be invoked.
The Meaning of "Type-Safe"
The term "type-safe" signifies that the C# compiler and runtime rigorously verify that the signature of the target method matches the signature defined by the delegate declaration.
Type safety is enforced in several ways:
- Return Type Verification: The method assigned to the delegate must return the exact type (or a compatible type via covariance) specified by the delegate.
- Parameter Matching: The number, order, and data
types (including modifiers like
refandout) of the parameters must strictly align with the delegate's definition. - Compile-Time Validation: If an attempt is made to assign an incompatible method to a delegate, the compiler throws an error immediately, preventing invalid method calls before the code ever runs.
Declaring and Using a Delegate
Using a custom delegate involves three primary steps: declaration, instantiation, and invocation.
using System;
// 1. Declare a delegate type with a specific signature
public delegate void LogMessage(string message);
class Program
{
static void Main()
{
// 2. Instantiate the delegate pointing to a compatible method
LogMessage logger = ConsoleLogger;
// 3. Invoke the method through the delegate
logger("Execution started.");
}
static void ConsoleLogger(string text)
{
Console.WriteLine($"[LOG]: {text}");
}
}If ConsoleLogger took an int instead of a
string, or returned a bool instead of
void, the compiler would reject the assignment.
Modern C# Alternatives: Action and Func
While custom delegates can be defined using the delegate
keyword, modern C# development frequently utilizes the built-in generic
delegate types provided in the System namespace:
Action<...>: Represents a method that takes zero or more parameters and does not return a value (void).Func<..., TResult>: Represents a method that takes zero or more parameters and returns a value of typeTResult.Predicate<T>: Represents a method that takes an object of typeTand returns abool.
These built-in types maintain the exact same type-safety and function-pointer behavior while eliminating the boilerplate code required to declare custom delegate types.