C# Delegates and Events

Learn C# Delegates and Events with practical examples, explanations, best practices, and real-world coding techniques.

Delegates and events are important C# features for passing behavior between methods and communicating between objects. A delegate is a type-safe reference to a method, allowing a method to be stored in a variable, passed as an argument, or invoked later. An event provides a controlled notification mechanism that allows one object to announce something that happened while other objects respond independently.

These concepts are widely used in application development. Delegates support callbacks, sorting rules, validation functions, and reusable operations. Events are useful for notifications such as a download completing, an order being processed, a user interacting with a component, or a background operation changing its status. Understanding the difference helps you design code that is flexible without creating unnecessary dependencies between classes.

Understanding Delegates in C#

A delegate defines the method signature that a referenced method must match. The signature includes the return type and parameter types. When a method matches that signature, it can be assigned to a compatible delegate and invoked through it.

Think of a delegate as a variable that holds a reference to a method instead of directly holding a value such as an integer or string. The code that invokes the delegate does not need to know the internal implementation of the referenced method. It only needs to know the delegate's signature.

public delegate int MathOperation(int first, int second);

static int Add(int first, int second)
{
    return first + second;
}

static int Multiply(int first, int second)
{
    return first * second;
}

MathOperation operation = Add;
Console.WriteLine(operation(4, 3)); // 7

operation = Multiply;
Console.WriteLine(operation(4, 3)); // 12

The MathOperation delegate accepts two integers and returns an integer. Both Add and Multiply match that signature, so either method can be assigned to the delegate. Calling operation executes whichever method it currently references.

This separation becomes useful when the calling code should choose an operation without being tightly connected to a specific implementation. For example, a calculation service can accept a delegate to determine how values should be combined without requiring a separate method for every possible operation.

Built-in Delegate Types: Action, Func, and Predicate

C# includes several generic delegate types that cover common scenarios. You do not need to declare a custom delegate for every callback. The built-in types make method signatures easier to reuse and understand.

  • Action: Represents a method that accepts zero or more parameters and returns no value.
  • Func: Represents a method that accepts parameters and returns a value. Its final generic type argument specifies the return type.
  • Predicate: Represents a method that accepts one value and returns a Boolean result, commonly used for testing a condition.
Action<string> greet = name =>
    Console.WriteLine($"Hello, {name}");

Func<int, int, int> add = (a, b) => a + b;

Predicate<int> isEven = number => number % 2 == 0;

greet("Abhinash");
Console.WriteLine(add(10, 5));        // 15
Console.WriteLine(isEven(8));         // True
Console.WriteLine(isEven(7));         // False

The Action<string> delegate accepts a string and returns no value. The Func<int, int, int> delegate accepts two integers and returns an integer. The Predicate<int> delegate accepts an integer and returns a Boolean.

Built-in delegates are particularly useful when passing behavior into methods. For example, a filtering operation can receive a Predicate<int> that decides which numbers should be retained, while a processing method can accept an Action<T> to execute for each item.

Using Delegates for Callbacks

A callback is a method supplied to another method so that it can be executed at a particular point. A delegate provides the type-safe mechanism that makes this pattern possible. Callbacks allow reusable code to control when an operation runs while leaving the specific behavior to the caller.

static void ProcessOrder(
    int orderId,
    Action<int> onCompleted)
{
    Console.WriteLine($"Processing order {orderId}.");

    // Perform the order processing work here.

    onCompleted(orderId);
}

ProcessOrder(1024, id =>
{
    Console.WriteLine($"Order {id} completed.");
});

The ProcessOrder method accepts an Action<int> callback. Once its processing work is complete, it invokes the callback with the order identifier. The caller decides what the callback should do, such as updating application state or displaying a notification.

This example illustrates the callback pattern; a real order-processing method should invoke the completion callback only after the operation has actually succeeded. Also consider whether the callback should be optional, represented by a nullable delegate, or replaced with a clearer return value or asynchronous operation. A callback is not automatically asynchronous simply because it is represented by a delegate.

What Are Events in C#?

An event is a publisher-subscriber mechanism built around delegates. The publisher exposes an event that announces something has happened. Subscribers register their event handlers and receive notifications when the publisher raises that event.

The event keyword restricts how other objects interact with a delegate-backed notification. External code can normally subscribe with += and unsubscribe with -=, but it cannot directly invoke the event or replace the publisher's complete invocation list. The publisher retains control over when the notification is raised.

Events therefore differ from public delegate fields. With a public delegate field, external code could overwrite previously registered callbacks or invoke the delegate itself. An event provides a clearer boundary between the object that announces a change and the objects that respond to it.

Creating and Raising an Event

A common .NET convention is to use EventHandler or EventHandler<TEventArgs> for notifications. The generic form allows the publisher to provide additional information about the event through an event-arguments object.

using System;

public sealed class DownloadCompletedEventArgs : EventArgs
{
    public string FilePath { get; }

    public DownloadCompletedEventArgs(string filePath)
    {
        FilePath = filePath;
    }
}

public sealed class Download
{
    public event EventHandler<DownloadCompletedEventArgs>? Completed;

    public void Finish(string filePath)
    {
        Console.WriteLine("Download finished.");

        Completed?.Invoke(
            this,
            new DownloadCompletedEventArgs(filePath));
    }
}

The Download class declares an event named Completed. When Finish() is called, the publisher raises the event and supplies itself as the sender, along with the file path in DownloadCompletedEventArgs.

The null-conditional operator ?. ensures that the invocation is skipped when there are no registered handlers. This avoids a null-reference error when an event has no subscribers. The null check does not guarantee that the event has subscribers at any other moment, which is why events commonly remain nullable.

Subscribing to and Unsubscribing from Events

A subscriber registers an event handler using +=. The handler must have a signature compatible with the event delegate. When the publisher raises the event, the registered handlers are invoked.

var download = new Download();

download.Completed += (sender, args) =>
{
    Console.WriteLine($"Saved file: {args.FilePath}");
};

download.Completed += (sender, args) =>
{
    Console.WriteLine("The UI can now refresh.");
};

download.Finish("reports/monthly.pdf");

Each subscribed handler receives the notification when Finish() raises the event. A publisher does not need to know the identities or implementation details of its subscribers. One handler might update a user interface, while another records a completed download.

You can remove a handler with -=, provided you use the same delegate instance that was registered. For an anonymous lambda, keep a reference to the delegate if it must later be removed. This is especially important for long-lived publishers and subscribers with shorter lifetimes.

EventHandler<DownloadCompletedEventArgs> handler =
    (sender, args) =>
        Console.WriteLine($"Completed: {args.FilePath}");

download.Completed += handler;

// Remove the same handler when it is no longer needed.
download.Completed -= handler;

A publisher's event subscription can keep a subscriber object reachable for as long as the publisher retains the handler. If the publisher outlives the subscriber, failing to unsubscribe may unintentionally extend the subscriber's lifetime. Consider explicit cleanup, disposal patterns, or carefully designed weak-event mechanisms when the application architecture requires them.

Delegates vs. Events

Delegates and events are related, but they solve different design problems. Use a delegate when you need to pass a method or behavior to another method. Use an event when an object needs to publish a notification that other objects can independently subscribe to.

  • Delegate: Suitable for callbacks, custom sorting rules, transformations, and operations supplied by a caller.
  • Event: Suitable for notifications such as completion, state changes, user interactions, and domain events within an application.
  • Return value: Often preferable when the caller simply needs the result of an operation.
  • Interface: Often preferable when a collaborator needs several related operations or a larger, explicit contract.

Events are generally synchronous by default. When an event is raised, its handlers run as part of that invocation, commonly on the publisher's current thread. A slow handler can delay the publisher, and an exception thrown by a handler can interrupt normal event invocation and prevent later handlers from being reached. If notifications require asynchronous processing, retries, guaranteed delivery, or independent failure handling, use an explicit asynchronous API or a suitable queueing strategy rather than assuming that an event provides those guarantees.

Best Practices for Delegates and Events

  • Prefer built-in delegates when appropriate: Use Action, Func, and Predicate for common callback signatures instead of declaring unnecessary custom delegate types.
  • Use meaningful names: Name event members after the occurrence they announce, such as Completed, StateChanged, or OrderProcessed.
  • Pass event data explicitly: Use a suitable event-arguments type when subscribers need information beyond the sender.
  • Manage subscriptions: Unsubscribe when a subscriber no longer needs notifications, especially when the publisher has a longer lifetime.
  • Avoid heavy event handlers: Keep synchronous handlers short. Move expensive work to an explicitly managed asynchronous workflow when necessary.
  • Do not use events for every interaction: A normal method call or return value is often clearer when there is only one caller and a direct result is needed.
  • Consider concurrent access: If subscriptions and notifications occur on multiple threads, design the subscriber's state and callback behavior for concurrency. Events do not automatically make application state thread-safe.

Delegates make methods usable as values, enabling flexible callback-based design. Events build on that capability to provide a controlled way for one object to notify multiple interested subscribers. Used appropriately, they reduce coupling, improve extensibility, and make C# applications easier to maintain.

Code Examples

static async Task<T> RetryAsync<T>(Func<Task<T>> action)
{
    return await action();
}
    
Action<string> log = message => Console.WriteLine(message);
Predicate<int> isPositive = number => number > 0;

log("Validating quantity.");
Console.WriteLine(isPositive(5));  // True
Console.WriteLine(isPositive(-2)); // False
    
using System;

public sealed class DownloadCompletedEventArgs : EventArgs
{
    public string FilePath { get; }

    public DownloadCompletedEventArgs(string filePath)
    {
        FilePath = filePath;
    }
}

public sealed class Download
{
    public event EventHandler<DownloadCompletedEventArgs>? Completed;

    public void Finish(string filePath)
    {
        Completed?.Invoke(
            this,
            new DownloadCompletedEventArgs(filePath));
    }
}
    
var download = new Download();

EventHandler<DownloadCompletedEventArgs> handler =
    (sender, args) =>
        Console.WriteLine($"Downloaded: {args.FilePath}");

download.Completed += handler;

download.Finish("report.pdf");

download.Completed -= handler;
    
static void ProcessOrder(int orderId, Action<int> onCompleted)
{
    Console.WriteLine($"Processing order {orderId}.");

    // Perform the required processing before notifying completion.
    onCompleted(orderId);
}

ProcessOrder(1024, id =>
{
    Console.WriteLine($"Order {id} completed.");
});
    

Pro Tip

Use delegates when a method needs behavior supplied by its caller, and events when an object announces that something happened. Store handler references when you need to unsubscribe, and remember that ordinary events execute synchronously unless you explicitly design an asynchronous workflow.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.