In this article, we are going to talk more about delegates in C#.

A delegate is a reference to a method. We can use a delegate object to pass it to the code in which we want to call a referenced method, without knowing at compile time which method will be invoked.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

If you want to see the complete navigation of this tutorial, you can do that here C# Intermediate Tutorial.

To download the source code, you can visit Delegates in C# Source Code. 

We are going to divide this article into the following sections:

Delegate Syntax

A base syntax to create a delegate object is:

delegate Result_Type identifier([parameters]);

There are three steps in defining and using delegates:

  • Declaration of our delegate
  • Instantiation, creating the delegate’s object
  • Invocation, where we call a referenced method
//Declaration
public delegate void WriterDelegate(string text);
class Program
{
    public static void Write(string text)
    {
        Console.WriteLine(text);
    }

    static void Main(string[] args)
    {
        //Instantiation
        WriterDelegate writerDelegate = new WriterDelegate(Write);

        //Invocation
        writerDelegate("Some example text.");
    }
}

It is important to understand that the return type of a method and the number of parameters must match the delegate’s return type and the number of parameters. Otherwise, we will get a compiler error. We can see in our example that our Write method has a void as return type and only one string parameter as well as our delegate.

Delegates are very useful in the encapsulation of our methods.

C# has the two built-in delegates: Func<T> and Action<T>, there are widely used, so let’s talk more about them.

Func<T> Delegate

This delegate encapsulates a method that has up to sixteen parameters and returns a value of the specified type. So, in other words, we use the Func delegate only with a method that has a return type other than void.

We can instantiate the Func delegate with this syntax:

Func<Type1, Type2..., ReturnType> DelegateName = new Func<Type1, Type2..., ReturnType>(MethodName);

We can see that the last parameter inside square brackets is a return type. Of course, we don’t have to initialize a delegate object like this, we can do it in another way:

Func< Type1, Type2..., ReturnType> name = MethodName;

Let’s see how to use Func delegate with an example:

class Program
{
    public static int Sum(int a, int b)
    {
        return a + b;
    }

    static void Main(string[] args)
    {
        Func<int, int, int> sumDelegate = Sum;
        Console.WriteLine(sumDelegate(10, 20));
    }
}

Action<T> Delegate

This delegate encapsulates a method that has up to sixteen parameters and doesn’t return any result. So we can assign to this delegate only methods with the void return type.

We can instantiate the Action object with this syntax:

Action<Type1, Type2...> DelegateName = new Action<Type1, Type2...>(MethodName);

Or, we can use another way:

Action < Type1, Type2...> DelegateName = MethodName;

Let’s see how to use Action delegate with an example:

public static void Write(string text)
{
    Console.WriteLine(text);
}

static void Main(string[] args)
{
    Action<string> writeDelegate = Write;
    writeDelegate("String parameter to write.");
}

Practical Example

In this example, we are going to create an application that executes one of three methods (Sum, Subtract, Multiply) based on a single provided parameter. Basically, if we send Sum as a parameter, the Sum method will be executed, and so on. First, we will write this example without delegates and then we will refactor that code by introducing delegates.

So let’s start with the first part:

public enum Operation
{
    Sum,
    Subtract,
    Multiply
}

public class OperationManager
{
    private int _first;
    private int _second;
    public OperationManager(int first, int second)
    {
        _first = first;
        _second = second;
    }

    private int Sum()
    {
        return _first + _second;
    }

    private int Subtract()
    {
        return _first - _second;
    }

    private int Multiply()
    {
        return _first * _second;
    }

    public int Execute(Operation operation)
    {
        switch (operation)
        {
            case Operation.Sum:
                return Sum();
            case Operation.Subtract:
                return Subtract();
            case Operation.Multiply:
                return Multiply();
            default:
                return -1; //just to simulate
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var opManager = new OperationManager(20, 10);
        var result = opManager.Execute(Operation.Sum);
        Console.WriteLine($"The result of the operation is {result}");

        Console.ReadKey();
    }
}

If we start this application, we will get the correct response for any operation we send to the Execute method. But this code could be much better and easier to read without switch-case expression. If we are going to have more than ten operations (for example), this switch block would be very ugly to read and maintain as well.

So, let’s change our code to make it readable, maintainable, and more object-oriented. Let’s introduce a new class ExecutionManager:

public class ExecutionManager
{
    public Dictionary<Operation, Func<int>> FuncExecute { get; set; }
    private Func<int> _sum;
    private Func<int> _subtract;
    private Func<int> _multiply;

    public ExecutionManager()
    {
        FuncExecute = new Dictionary<Operation, Func<int>>(3);
    }

    public void PopulateFunctions(Func<int> Sum, Func<int> Subtract, Func<int> Multiply)
    {
        _sum = Sum;
        _subtract = Subtract;
        _multiply = Multiply;
    }

    public void PrepareExecution()
    {
        FuncExecute.Add(Operation.Sum, _sum);
        FuncExecute.Add(Operation.Subtract, _subtract);
        FuncExecute.Add(Operation.Multiply, _multiply);
    }
}

Here, we create a dictionary that will hold all the operations and all the references towards our methods (Func delegates). Now we can inject this class into the OperationManager class and change the Execute method:

public class OperationManager
{
    private int _first;
    private int _second;
    private readonly ExecutionManager _executionManager;

    public OperationManager(int first, int second, ExecutionManager executionManager)
    {
        _first = first;
        _second = second;
        _executionManager = executionManager;
        _executionManager.PopulateFunctions(Sum, Subtract, Multiply);
        _executionManager.PrepareExecution();
    }

    private int Sum()
    {
        return _first + _second;
    }

    private int Subtract()
    {
        return _first - _second;
    }

    private int Multiply()
    {
        return _first * _second;
    }

    public int Execute(Operation operation)
    {
        return _executionManager.FuncExecute.ContainsKey(operation) ?
            _executionManager.FuncExecute[operation]() :
            -1;
    }
}

Now, we are configuring all in the constructor of the OperationManager class and executing our action in the Execute method if it contains the required operation. At the first look, we can see how much better this code is.

Finally, we need to change the Program class:

class Program
{
    static void Main(string[] args)
    {
        var executionManager = new ExecutionManager();
        var opManager = new OperationManager(20, 10, executionManager);
        var result = opManager.Execute(Operation.Sum);
        Console.WriteLine($"The result of the operation is {result}");

        Console.ReadKey();
    }
}

Conclusion

In this article, we have learned:

  • How to instantiate a delegate
  • The way to use Func and Action delegates
  • How to write a better code by using delegates

If you have been with us along with this entire intermediate series, you’ve hopefully mastered OOP concepts in C#.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!