Author avatar

Scott Hannen

How and Why to Use Delegates in C#

Scott Hannen

  • Sep 16, 2019
  • 7 Min read
  • 36 Views
  • Sep 16, 2019
  • 7 Min read
  • 36 Views
Languages Frameworks and Tools
C#

Introduction

It's possible to write code for years without deliberately using delegate, Action, or Func types. I say "deliberately" because we may have used them without realizing it.

Knowing what these types represent makes reading code easier. Knowing how to use them adds some useful tools to our developer toolbox.

What Is a `delegate`?

A delegate is a type that represents a method with a specific signature and return type.

The declaration of a delegate looks exactly like the declaration of a method, except with the keyword delegate in front of it.

Examples:

  • A delegate representing a method that adds two numbers and returns a result:
    delegate int AddNumbers(int value1, int value2);

  • A delegate representing a method that logs an exception and doesn't return anything:
    delegate void LogException(Exception ex);

  • A delegate representing a function that converts some generic type to a string:
    delegate string FormatAsString<T>(T input);

Just like classes and interfaces, we can declare delegates outside of classes or nested within classes. We can mark them private, public, or internal.

What Can We Assign to a `delegate`?

We can assign a reference to any method that matches the delegate's signature.

For example, suppose we have the following delegate and class:

1
2
3
4
5
6
7
8
delegate double MathCalculation(float value1, float value2);

public static class Calculator
{
    public static double AddNumbers(float value1, float value2) => value2 + value2;

    public static double DivideNumbers(float value1, float value2) => value1 / value2;
}
csharp

We can assign either method to a variable declared as type MathCalculation:

1
2
MathCalculation add = Calculator.AddNumbers;
MathCalculation divide = Calculator.DivideNumbers;
csharp

How Do We "Call" a `delegate`?

Calling the method referenced by a delegate is called invoking the delegate. We can do this with the Invoke method:

1
var result = add.Invoke(2, 3);
csharp

Or without the Invoke method:

1
var result = divide(100, 3);
csharp

What Are `Action` and `Func`?

Action and Func are delegates that we can use instead of defining our own delegate types. That's important to remember: Action and Func are delegates.

For example, instead of declaring the MathCalculation delegate and assigning the following,

1
MathCalculation add = Calculator.AddNumbers;
csharp

We could assign:

1
Func<float, float, double> add = Calculator.AddNumbers;
csharp

We use Func<> to represent a method that returns something. If the function has parameters, the first generic argument(s) represent those parameters. The last generic argument indicates the return type. Func<int, DateTime, string> is a function with an int and DateTime parameter that returns a string.

Action and Action<> represent methods that return nothing. Action has no parameters. Action<string, int> represents a method with a string and int parameter.

Should I Use Action or Func or Declare a delegate?

Action or Func or declaring a delegate are, essentially, interchangeable. Declaring a delegate allows us to give it a name that indicates what it's for. A delegate called MathCalculation is clearly intended to do math. Func<float, float, double> doesn't tell us what the method does.

It's a preference and a case-by-base decision. delegate may be clearer, but Action and Func save us from having to declare more delegate types and usually suffice when the use is obvious.

We Can Pass `delegate`, `Action`, and `Func` Just Like Other Types

If you use LINQ, you've already done this. Consider this example:

1
2
int[] numbers = {1, 5, 1000, 10};
var bigNumbers = numbers.Where(n => n > 999);
csharp

We're actually creating an anonymous Func<int, bool> - a function that takes an int and returns true or false - and passing the function as a parameter to the Where method. Then the Where method executes that function for each item in the list to see if it's true or false.

If our class had a function with the same signature, like:

1
bool IsBigNumber(int number) => number > 499;
csharp

Then we could pass that function as a parameter to Where:

1
var bigNumbers = numbers.Where(IsBigNumber);
csharp

We can write our own methods that have functions as parameters. For example, this generic method takes a List<T> and a Func<T, bool> and uses it to return all of the items from the list where the condition is not true:

1
2
3
4
List<T> Exclude<T>(List<T> values, Func<T, bool> condition)
{
    return values.Where(value => !condition(value)).ToList();
}
csharp

It works by taking the Func<T, bool> that was passed to the Exclude method and passing that function to the Where method.

Functions That Return Functions

We can also return functions from a function. Suppose we're going to filter and sort a collection, but we want to change the sort order based on a variable. We might have an enum for different sort orders:

1
public enum SortOrder { FirstName, LastName, BirthDate }
csharp

When we call LINQ's OrderBy method, we pass to it a function that returns the value by which to sort. We could write a function that uses a variable and determines which property of a Person by which to sort:

1
2
3
4
5
6
7
8
9
10
11
12
Func<Person, IComparable> GetSortFunction(SortOrder sortOrder)
{
    switch (sortOrder)
    {
        case SortOrder.FirstName:
            return person => person.FirstName;
        case SortOrder.BirthDate:
            return person => person.BirthDate;
        default:
            return person => person.LastName;
    }
}
csharp

Then we can use that variable as follows:

1
2
3
4
5
IEnumerable<Person> SortAndFilter(IEnumerable<Person> people, SortOrder sortOrder)
{
    var sortFunction = GetSortFunction(sortOrder);
    return people.Where(person => person.Active).OrderBy(sortFunction);
}
csharp

We're returning a delegate (a Func<Person, IComparable>) from one function, and using it as a parameter for another function (OrderBy).

Conclusion

Hopefully, this guide accomplishes the following two objectives:

  1. We've taken some of the mystery out of delegate, as well as Action and Func; both of which represent delegates. When you see these used in code, it's easier to tell what's happening.

  2. We've added a new tool which, when used judiciously, can help us to write more concise, "functional" code.

To learn more about writing functional code in C#, watch Functional Programming with C# by Dave Fancher.

0