Delegate Declaration and Usage

Vaibhav • September 11, 2025

In the previous article, we introduced delegates as a way to treat methods as first-class citizens in C#. Now it’s time to dive deeper and understand how to declare delegate types, instantiate them, and invoke them in real-world scenarios. This article builds on your understanding of methods and parameters, and shows how delegates can make your code more flexible, extensible, and expressive.

We’ll walk through the syntax of delegate declarations, explore how to assign methods to delegates, and discuss how delegates behave under the hood. You’ll also learn how to use delegates with instance methods, static methods, and even anonymous functions - all while keeping your code type-safe and readable.

Declaring a Delegate Type

A delegate type defines the method signature that any assigned method must match. This includes the return type and the parameter list. Declaring a delegate is similar to declaring a method - but instead of writing a body, you’re defining a type.

public delegate void Logger(string message);

This defines a new type called Logger. Any method that takes a single string parameter and returns void can be assigned to a variable of this type.

Delegate types are reference types. You can pass them around like objects, store them in fields, and invoke them dynamically.

Assigning Methods to Delegates

Once you’ve declared a delegate type, you can create a delegate instance by assigning a method that matches the signature. Let’s look at a simple example:

void ConsoleLogger(string msg)
{
    Console.WriteLine("Log: " + msg);
}

Logger log = ConsoleLogger;
log("System started."); // Output: Log: System started.

The method ConsoleLogger matches the Logger delegate’s signature, so it can be assigned directly. When you invoke log("System started."), it calls ConsoleLogger with that argument.

Using Delegates with Instance Methods

Delegates can point to both static and instance methods. When pointing to an instance method, the delegate stores both the method and the target object. Here’s an example:

class Printer
{
    public void Print(string text)
    {
        Console.WriteLine("Printing: " + text);
    }
}

Printer p = new Printer();
Logger log = p.Print;
log("Hello world"); // Output: Printing: Hello world

The delegate log stores a reference to the Print method and the instance p. When invoked, it calls p.Print with the given argument.

Note: Delegates maintain the target object internally. This allows them to invoke instance methods even when the original object is out of scope.

Invoking Delegates

You invoke a delegate just like a method - using parentheses and passing arguments. Behind the scenes, the runtime checks that the delegate is not null and then calls the referenced method.

Logger log = ConsoleLogger;
log("Initialization complete.");

This is equivalent to calling ConsoleLogger("Initialization complete."), but with the added flexibility of indirection.

Always check if a delegate is null before invoking it, especially in event-driven code. Use the null-conditional operator (?.) to avoid exceptions.

log?.Invoke("Safe call");

Delegates as Parameters

One of the most powerful uses of delegates is passing them as parameters to other methods. This allows you to inject behavior into a method - a key idea in functional programming and design patterns.

void ProcessData(string data, Logger logger)
{
    // Simulate processing
    logger("Processing: " + data);
}

ProcessData("Sensor reading", ConsoleLogger);

The method ProcessData accepts a delegate and uses it to log messages. This makes the logging behavior customizable - you can pass different loggers depending on context.

Delegates and Return Values

Delegates can also return values. Let’s define a delegate that performs a calculation:

public delegate int Calculator(int x, int y);

int Multiply(int a, int b) => a * b;

Calculator calc = Multiply;
int result = calc(3, 4); // result == 12

The delegate Calculator returns an int, and Multiply matches the signature. You can now use calc to perform multiplication.

Combining Delegates

Delegates support multicast - meaning you can combine multiple delegates into one. When invoked, all methods are called in order. This is useful for event handling and notification systems.

void LogToConsole(string msg) => Console.WriteLine("Console: " + msg);
void LogToFile(string msg) => Console.WriteLine("File: " + msg); // Simulated

Logger combined = LogToConsole;
combined += LogToFile;

combined("System update"); 
// Output:
// Console: System update
// File: System update

The += operator adds a method to the invocation list. When combined is called, both methods run in sequence.

Multicast delegates ignore return values. If your delegate has a return type, only the result of the last method is returned. Use multicast only when return values aren’t needed.

Removing Methods from Delegates

You can remove methods from a delegate using the -= operator. This is useful for unsubscribing from events or changing behavior dynamically.

combined -= LogToFile;
combined("After removal"); 
// Output:
// Console: After removal

Now only LogToConsole is invoked. The delegate’s invocation list is updated at runtime.

Delegates and Type Inference

In modern C#, you can use method group conversions to assign methods to delegates without explicitly using new. This makes the syntax cleaner and more intuitive.

Logger log = ConsoleLogger; // Implicit conversion
Calculator calc = Multiply; // No need for new Calculator(Multiply)

The compiler infers the delegate type from the method signature. This works only when the method matches the delegate exactly.

Delegates vs Interfaces

You might wonder - when should I use a delegate, and when should I use an interface? Delegates are ideal for short-lived, pluggable behavior - like callbacks, filters, or event handlers. Interfaces are better for long-term contracts and polymorphism.

For example, a sorting algorithm might accept a delegate for comparison logic, while a payment system might use an interface to define payment processors.

Use delegates when you need to pass behavior as a parameter. Use interfaces when you need to define a set of related behaviors across multiple classes.

Summary

In this article, we explored how to declare and use delegates in C#. You learned how to define delegate types, assign methods, invoke delegates, and pass them as parameters. We covered both static and instance methods, return values, multicast behavior, and type inference.

Delegates are a powerful tool for decoupling logic and injecting behavior. They enable flexible design patterns and functional programming techniques. In the next article, we’ll explore Multicast Delegates in more detail - including how they work internally and when to use them effectively.