Delegates — Type-Safe Function Pointers

A delegate is a type-safe object that holds a reference to a method — a “typed function pointer.” You can pass delegates as parameters, store them in variables, and invoke them later without knowing which specific method will run. This is the mechanism behind every callback pattern in .NET: LINQ’s Where(predicate), ASP.NET Core’s middleware pipeline, event handlers, and async continuations. Understanding delegates is the prerequisite for understanding lambdas, events, and the entire functional programming side of C#.

Declaring and Using Delegates

// ── Declare a delegate type — defines the method signature ────────────────
public delegate bool Validator(string value);
public delegate void Logger(string message, LogLevel level);
public delegate int Transformer(int input);

// ── Create delegate instances — point to methods ──────────────────────────
bool IsValidEmail(string value) => value.Contains('@') && value.Contains('.');
bool IsNotEmpty(string value)   => !string.IsNullOrWhiteSpace(value);

Validator emailValidator = IsValidEmail;
Validator notEmpty       = IsNotEmpty;

// ── Invoke delegates ───────────────────────────────────────────────────────
bool result1 = emailValidator("alice@example.com");  // true
bool result2 = emailValidator("notanemail");          // false

// Null-safe invocation
bool result3 = emailValidator?.Invoke("test@test.com") ?? false;

// ── Delegate as method parameter (callback pattern) ───────────────────────
public static string Process(string input, Validator validate, Transformer transform)
{
    if (!validate(input))
        throw new ArgumentException($"Invalid input: {input}");
    return transform(int.Parse(input)).ToString();
}

// ── Multicast delegates — chain multiple methods ──────────────────────────
void LogToConsole(string msg, LogLevel lvl) => Console.WriteLine($"[{lvl}] {msg}");
void LogToFile   (string msg, LogLevel lvl) => File.AppendAllText("app.log", $"[{lvl}] {msg}\n");

Logger multiLogger  = LogToConsole;
multiLogger        += LogToFile;    // both methods called on invoke

multiLogger("Application started", LogLevel.Information);
// Calls LogToConsole AND LogToFile

multiLogger -= LogToFile;          // remove one handler
multiLogger("Now only console", LogLevel.Information);
Note: Delegates in .NET are reference types — they are objects that wrap one or more method references. A multicast delegate (one with multiple methods added via +=) stores an invocation list: an array of method references that are called in order when the delegate is invoked. If any method in the invocation list throws an exception, the remaining methods are not called. For event handlers where you want all subscribers to run regardless, use a try/catch inside each handler.
Tip: In modern C#, you rarely need to declare custom delegate types. The built-in generic delegates Action<T> (void return), Func<T, TResult> (non-void return), and Predicate<T> (returns bool) cover almost every case. Only declare a custom delegate type when: (1) you need a named type for documentation or clarity, (2) you need ref/out parameters (generics don’t support them cleanly), or (3) you are defining an event delegate that follows the EventHandler<TEventArgs> pattern.
Warning: Multicast delegates with a non-void return type only return the result of the last method in the invocation list — previous return values are silently discarded. This is rarely the desired behaviour. If you need the results of all invocations, iterate the invocation list manually: foreach (var handler in myDelegate.GetInvocationList()) { var result = ((MyDelegate)handler)(arg); }. For void delegates (event handlers, callbacks), multicast is the standard and expected pattern.

Delegate Compatibility

// Any method with the matching signature can be assigned to a delegate
public delegate int MathOp(int a, int b);

int Add(int a, int b) => a + b;
int Subtract(int a, int b) => a - b;
int Multiply(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(10, 3));   // 13

op = Subtract;
Console.WriteLine(op(10, 3));   // 7

// Store in a dictionary for a command dispatch table
var operations = new Dictionary<string, MathOp>
{
    ["+"] = Add,
    ["-"] = Subtract,
    ["*"] = Multiply,
};

string symbol = "+";
int result = operations[symbol](10, 5);   // 15

Common Mistakes

Mistake 1 — Declaring custom delegate types when Action/Func suffice

❌ Wrong — unnecessary custom delegate type:

public delegate void Processor(string input);   // redundant

✅ Correct — use built-in Action:

Action<string> processor = input => DoWork(input);

Mistake 2 — Not checking for null before invoking a delegate

❌ Wrong — NullReferenceException if no handlers are subscribed:

myDelegate("data");   // throws if myDelegate is null

✅ Correct — null-conditional invocation:

myDelegate?.Invoke("data");   // safe — does nothing if null

🧠 Test Yourself

A multicast delegate has three methods subscribed. The second method throws an exception. What happens to the third method?