Skip to content

Delegates in C#

Published: at 05:53 PM

In C#, one of the most powerful tools at our disposal for passing functions around is the concept of delegates. Delegates are what truly enables functional programming within the object-oriented paradigm of C#. This post will delve into what delegates are, how they work, and how they bridge the gap between object-oriented programming (OOP) and functional programming in C#.

What Are Delegates?

A delegate in C# is a type that represents references to methods with a specific signature. It allows you to encapsulate a method (or methods) inside a delegate object, and this delegate can be passed around and invoked, just like a method. Essentially, delegates treat functions as types, enabling the use of methods as parameters, return values, or as fields within a class.

Naming Function Signatures with Delegates

Delegates allow us to define the signature of a method we want to reference. For example, consider the built-in EventHandler delegate in C#, commonly used for event handling.

Observe the functional notation of the EventHandler delegate’s signature:

EventHandler: (object, EventArgs) -> void

This notation indicates that an EventHandler is any method that takes an object and an EventArgs instance as parameters and returns void.

The actual delegate definition in C# looks like this:

public delegate void EventHandler(
    object sender,
    EventArgs e
);

If you’re familiar with abstract classes, this syntax might look similar. However, instead of using the abstract keyword, we use the delegate keyword to define a delegate. The definition includes:

How Delegates Bridge OOP and Functional Programming

C#, being an object-oriented language, typically doesn’t emphasize function signatures beyond method overload resolution. This is where delegates come in, serving as the bridge between OOP and functional programming.

Delegates are essentially a compiler trick. When you define a delegate, the compiler generates a new type that inherits from the built-in MulticastDelegate class. This generated type includes an Invoke method that has the same signature as the delegate, allowing you to call the encapsulated methods as if they were regular methods.

Here’s how it works:

In other words, the compiler wraps the method calls into a generated class, abstracting away the complexities. This allows us to pass methods around as if they were objects, seamlessly integrating functional programming concepts into the OOP paradigm.

Evolution of Delegates

Delegates in C# have evolved over time, becoming more powerful and versatile. In this section, we’ll explore a practical example to demonstrate how delegates can be used.

Example: Currency Converter Using Delegates

Consider the following example, where we use a delegate to create a simple currency converter:

// Basic converter from GBP to various currencies
public static Func<decimal, decimal> GetConverter(string currency)
{
    switch (currency)
    {
        case "USD": return x => x * 1.15m;
        case "BTC": return x => x * 0.000058m;
        // More cases can be added here
        default: throw new ArgumentException("Unsupported currency");
    }
}

private static decimal Eval(decimal amount, string currency)
{
    return GetConverter(currency)(amount);
}

In this example, the GetConverter method returns a Func<decimal, decimal>, which is a delegate that takes a decimal and returns a decimal. The method uses a switch statement to determine which conversion function to return based on the input currency.

You can use the Eval method like this:

var usdAmount = Eval(123.34m, "USD");
var btcAmount = Eval(4325m, "BTC");

Breaking Down the Example

  1. Delegate Definition: We define the delegate using the Func notation, which is a built-in delegate type in C#. The Func<decimal, decimal> delegate indicates that the method takes a decimal as input and returns a decimal.

  2. Switch Statement: Depending on the currency provided, the GetConverter method returns a lambda expression that performs the conversion. The lambda expression is an inline function, and the => operator is read as “goes to,” indicating that the input (x) is mapped to the expression on the right-hand side.

  3. Type Inference: Notice that we don’t explicitly specify the types of the input parameters in the lambda expressions. The compiler infers these types from the delegate definition.

  4. Evaluation: The Eval method then calls the appropriate conversion function returned by GetConverter, passing in the amount to be converted.

Conclusion

Delegates are a fundamental feature of C#, allowing developers to encapsulate method references, pass them around, and invoke them dynamically. By bridging the gap between object-oriented and functional programming, delegates provide the flexibility needed to write clean, maintainable, and reusable code.

Whether you’re handling events, implementing callback functions, or simply want to pass methods around as parameters, understanding delegates will greatly enhance your ability to write effective C# applications.