Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A delegate in C# is a type-safe, object-oriented reference type that encapsulates a reference to a method with a specific signature and return type. Unlike traditional C++ function pointers, delegates are fully object-oriented, strictly type-checked by the compiler, and encapsulate both a method and, in the case of instance methods, the object instance on which the method is invoked. Under the hood, every delegate you define is compiled into a class that implicitly derives from System.MulticastDelegate, which itself inherits from System.Delegate. Because they are reference types, delegates can be assigned to variables, passed as arguments, and returned from methods.

Syntax and Mechanics

Working with a delegate involves three distinct phases: declaration, instantiation, and invocation. When instantiating a delegate, modern C# provides three primary mechanisms to bind a method to the delegate instance:
  1. Method Group Conversion: Directly assigning an existing named method.
  2. Anonymous Methods: Defining an inline, unnamed method using the delegate keyword.
  3. Lambda Expressions: A concise syntax (=>) for creating anonymous functions, where the compiler infers parameter types and return types based on the delegate’s signature.
using System;

// 1. Declaration: Defines the required method signature and return type.
// Any method assigned to this delegate MUST take two ints and return an int.
public delegate int BinaryOperation(int x, int y);

public class DelegateMechanics
{
    // A static method matching the BinaryOperation signature
    public static int Add(int a, int b) => a + b;

    public void Execute()
    {
        // 2. Instantiation
        
        // Mechanism A: Method Group Conversion
        // The compiler implicitly converts the method group 'Add' to the delegate type.
        BinaryOperation op1 = Add;

        // Mechanism B: Anonymous Method
        // Explicitly defines an inline method without a name.
        BinaryOperation op2 = delegate(int a, int b) { return a + b; };

        // Mechanism C: Lambda Expression
        // The most common modern approach. Types are inferred from the delegate signature.
        BinaryOperation op3 = (a, b) => a + b;

        // 3. Invocation: Executing the referenced method(s).
        // This compiles down to op1.Invoke(10, 5);
        int result = op1(10, 5); 
    }
}

Nullability and Safe Invocation

Because delegates are reference types, their default value is null. Attempting to invoke a null delegate directly throws a NullReferenceException. To prevent this, modern C# utilizes the null-conditional operator (?.) combined with the delegate’s explicit Invoke method. For delegates with a void return type, this null-conditional invocation evaluates to void (producing no value). It simply short-circuits and skips the execution entirely as a statement.
using System;

public class NullSafeDemonstration
{
    public delegate void NotificationHandler(string message);

    public void Execute()
    {
        NotificationHandler handler = null;

        // Unsafe: Throws NullReferenceException
        // handler("System starting..."); 

        // Safe: Short-circuits and skips execution as a statement.
        handler?.Invoke("System starting...");
    }
}

Multicasting and Invocation Lists

Because C# delegates derive from System.MulticastDelegate, a single delegate instance can hold an array of method references, known as an invocation list. Methods are added to or removed from the invocation list using the += and -= operators (which compile down to Delegate.Combine and Delegate.Remove). When a multicast delegate is invoked, it executes the methods in its invocation list synchronously, in the exact order they were added.
using System;
using System.Diagnostics;

public delegate void LogHandler(string message);

public class MulticastDemonstration
{
    public void WriteToConsole(string msg) => Console.WriteLine($"Console: {msg}");
    public void WriteToDebug(string msg) => Debug.WriteLine($"Debug: {msg}");

    public void Execute()
    {
        LogHandler logger = WriteToConsole;
        
        // Appending a second method to the invocation list
        logger += WriteToDebug; 

        // Safely invokes WriteToConsole, then WriteToDebug sequentially
        logger?.Invoke("System fault detected."); 

        // Removing a method from the invocation list
        logger -= WriteToConsole;
    }
}
Exception Handling in Multicast Delegates: If any method in the invocation list throws an unhandled exception, the synchronous execution terminates immediately. The exception propagates to the caller, and any subsequent methods remaining in the invocation list are not executed. Return Values in Multicast Delegates: If a multicast delegate has a non-void return type, the caller receives the return value of the last method executed in the invocation list. The return values of all preceding methods are discarded.

The Target and Method Properties

A delegate instance tracks its underlying method via two primary properties inherited from System.Delegate:
  1. Method: A System.Reflection.MethodInfo object representing the signature and metadata of the targeted method.
  2. Target: An object reference.
    • If the delegate points to an instance method, Target holds a reference to the specific class instance the method belongs to.
    • If the delegate points to a static method, Target evaluates to null.

Built-in Generic Delegates

To eliminate the need for declaring custom delegate types for every unique signature, the .NET framework provides three highly generic, built-in delegate families in the System namespace:
  • Action and Action<T...>: Encapsulates a method that returns void. The non-generic System.Action takes exactly zero parameters, while the generic versions take from 1 up to 16 parameters.
  • Func<TResult> and Func<T..., TResult>: Encapsulates a method that returns a value of type TResult. The zero-parameter System.Func<TResult> takes no arguments, while the generic versions take from 1 up to 16 parameters. The last type parameter always represents the return type.
  • Predicate<T>: Encapsulates a method that takes exactly one parameter and returns a bool.
using System;

public class BuiltInDelegates
{
    public void Execute()
    {
        // System.Action (0 parameters, returns void)
        Action initialize = () => Console.WriteLine("Initializing...");

        // System.Action<T> (1 parameter, returns void)
        Action<string> printAction = Console.WriteLine;

        // System.Func<TResult> (0 parameters, returns string)
        Func<string> getStatus = () => "Active";

        // System.Func<T1, T2, TResult> (2 parameters, returns string)
        Func<int, int, string> formatSum = (a, b) => (a + b).ToString();

        // System.Predicate<T> (1 parameter, returns bool)
        Predicate<int> isEven = number => number % 2 == 0;
        
        initialize?.Invoke();
        printAction?.Invoke(formatSum?.Invoke(5, 5));
    }
}
Master C# with Deep Grasping Methodology!Learn More