C# Delegates and Events: Core Programming Concepts

Delegates represent one of the fundamental concepts in .NET programming, serving as powerful mechanisms for method invocation. At their core, delegates enable asynchronous and synchronous method calls, allow multiple methods to be invoked simultaneously, and facilitate passing methods as parameters for callback operations. Since program execution essentially involves inter-method communication, delegates become an essential skill for .NET developers. Events in .NET build upon delegate foundations, making delegate comprehension crucial for mastering event handling.

Understanding .NET Delegates

In everyday language, "delegation" implies entrusting some one else to perform a task. While this typically describes an action, within .NET, a delegate represents an entity—a proxy or intermediary. Consider a scenario where Party A needs services from Party B, but instead of contacting B directly, A engages Intermediary C to handle the transaction. In this analogy, traditional delegation refers to A's act of engaging C, whereas .NET delegates correspond to the intermediary C itself.

Delegate Concept VisualizationIn this diagram, A represents the requesting party, B the service provider, and C the .NET delegate acting as the intermediary. Since C serves as the middleman, it necessarily contains information about B to facilitate the connection.

Delegate Structure

Delegates function by substituting for requesters to engage service providers, translating to method invocation in programming contexts. To call a method programmatically, you need knowledge of the target method and its owner (for instance methods).

A delegate must minimal contain:

  • Method: The method to invoke
  • Target: The method's owner (null for static methods)

Delegates constitute data structures that function as specialized types inheriting from the common Delegate base class. All custom delegate types derive from MulticastDelegate, which extends Delegate.

Delegate declaration follows this pattern:

public delegate ReturnType DelegateName(ParameterType parameter);

This syntax mirrors regular method declarations, incorporating access modifiers, return types, parameters, and method names, prefixed with the delegate keyword.

Once defined, delegates can be instantiated using the new operator:

class Calculator
{
    public int PerformDivision(int dividend, int divisor)
    {
        return dividend / divisor;
    }
}

private delegate int DivisionOperation(int value1, int value2);

class Application
{
    static void Execute()
    {
        Calculator calc = new Calculator();
        DivisionOperation operation = new DivisionOperation(calc.PerformDivision);
        int outcome = operation(20, 4);
        Console.WriteLine($"Result: {outcome}");
    }
}

Delegate Chains

While single delegates wrap individual methods, delegate chains enable multiple method invocations. Methods can be appended to delegate instances using the += operator:

class Application
{
    static void HandlerOne(object source, EventArgs args)
    {
        Console.WriteLine("Executing Handler One");
    }

    static void HandlerTwo(object source, EventArgs args)
    {
        Console.WriteLine("Executing Handler Two");
    }

    static void HandlerThree(object source, EventArgs args)
    {
        Console.WriteLine("Executing Handler Three");
    }

    static void Execute()
    {
        EventHandler handler = new EventHandler(HandlerOne);
        handler += HandlerTwo;
        handler += new EventHandler(HandlerThree);
        handler -= HandlerTwo;
        handler(null, null);
        // Output:
        // Executing Handler One
        // Executing Handler Three
    }
}

This demonstrates that delegates can invoke multiple methods sequentially, maintain invocation order, and support method removal without affecting other registered methods.

Immutability of Delegates

Like strings, delegates exhibit immutability—once created, their internal state cannot change. Operations like appending or removing methods generate new delegate instances rather than modifying existing ones:

EventHandler primaryHandler = new EventHandler(MethodOne);
EventHandler temporary = primaryHandler;
EventHandler secondaryHandler = new EventHandler(MethodTwo);
primaryHandler += secondaryHandler; // Creates new instance

Each modification produces a fresh delegate containing combined method lists, leaving original delegates unchanged.

Events and Their Relationship to Delegates

Events build upon delegate functionality but impose access restrictions. While public delegate members permit external invocation, events restrict invocation to internal type methods. External code can only register and unregister hendlers:

public event Action<int int=""> OperationCompleted;

// Internal invocation only
protected virtual void OnOperationCompleted(int param1, int param2)
{
    OperationCompleted?.Invoke(param1, param2);
}
</int>

Event Programming Best Practices

Unregistering Events

Proper event unregistration prevents memory leaks. Since delegates maintain references to their targets, failing to unregister creates persistent references that prevent garbage collection.

Thread-Safe Event Handling

In multi-threaded environments, use local variables to avoid race conditions:

Action<int int=""> temporary = SomeEvent;
if (temporary != null)
{
    temporary(argument1, argument2);
}
</int>

Individual Delegate Invocation

To ensure all event handlers execute despite potential exceptions in individual handlers:

var handlerList = SomeEvent?.GetInvocationList();
foreach (var handler in handlerList)
{
    try
    {
        ((Action<int int="">)handler)(arg1, arg2);
    }
    catch
    {
        // Handle exception
    }
}
</int>

Weak References and Weak Delegates

Strong vs. Weak References

Strong references prevent garbage collection of referenced objects. Weak references allow collection while still providing access when the object remains alive:

WeakReference weakRef = new WeakReference(targetObject);
targetObject = null;

// Later, check if object still exists
if (weakRef.IsAlive)
{
    var retrievedObject = weakRef.Target;
}

Implementing Weak Delegates

Weak delegates replace strong target references with weak references, preventing memory leaks:

class WeakCallback
{
    private WeakReference _targetReference;
    private MethodInfo _methodInfo;

    public WeakCallback(Delegate source)
    {
        _targetReference = new WeakReference(source.Target);
        _methodInfo = source.Method;
    }

    public object Execute(params object[] parameters)
    {
        if (_targetReference.IsAlive)
        {
            return _methodInfo.Invoke(_targetReference.Target, parameters);
        }
        return null;
    }
}

This approach breaks the strong reference cycle between publishers and subscribers in event systems, particularly useful when subscribers might not properly unregister from long-lived publishers.

Appropriate Use Cases for Weak Delegates

Weak delegates excel in scenarios involving:

  • Long-lived publishers with potentially short-lived subscribers
  • UI controls with numerous event handlers
  • Systems where proper unregistration cannot be guaranteed

By implementing weak delegates, applications can avoid common memory leak patterns that occur when event publishers maintain strong references to subscribers who fail to unregister properly.

Tags: csharp Delegates events multithreading memory-management

Posted on Tue, 15 Sep 2026 16:03:50 +0000 by FSGr33n