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.
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);
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
.
We can assign a reference to any method that matches the delegate's signature.
For example, suppose we have the following delegate and class:
1delegate double MathCalculation(float value1, float value2);
2
3public static class Calculator
4{
5 public static double AddNumbers(float value1, float value2) => value2 + value2;
6
7 public static double DivideNumbers(float value1, float value2) => value1 / value2;
8}
We can assign either method to a variable declared as type MathCalculation
:
1MathCalculation add = Calculator.AddNumbers;
2MathCalculation divide = Calculator.DivideNumbers;
Calling the method referenced by a delegate is called invoking the delegate. We can do this with the Invoke
method:
1var result = add.Invoke(2, 3);
Or without the Invoke
method:
1var result = divide(100, 3);
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,
1MathCalculation add = Calculator.AddNumbers;
We could assign:
1Func<float, float, double> add = Calculator.AddNumbers;
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.
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.
If you use LINQ, you've already done this. Consider this example:
1int[] numbers = {1, 5, 1000, 10};
2var bigNumbers = numbers.Where(n => n > 999);
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:
1bool IsBigNumber(int number) => number > 499;
Then we could pass that function as a parameter to Where
:
1var bigNumbers = numbers.Where(IsBigNumber);
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:
1List<T> Exclude<T>(List<T> values, Func<T, bool> condition)
2{
3 return values.Where(value => !condition(value)).ToList();
4}
It works by taking the Func<T, bool>
that was passed to the Exclude
method and passing that function to the Where
method.
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:
1public enum SortOrder { FirstName, LastName, BirthDate }
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:
1Func<Person, IComparable> GetSortFunction(SortOrder sortOrder)
2{
3 switch (sortOrder)
4 {
5 case SortOrder.FirstName:
6 return person => person.FirstName;
7 case SortOrder.BirthDate:
8 return person => person.BirthDate;
9 default:
10 return person => person.LastName;
11 }
12}
Then we can use that variable as follows:
1IEnumerable<Person> SortAndFilter(IEnumerable<Person> people, SortOrder sortOrder)
2{
3 var sortFunction = GetSortFunction(sortOrder);
4 return people.Where(person => person.Active).OrderBy(sortFunction);
5}
We're returning a delegate (a Func<Person, IComparable>
) from one function, and using it as a parameter for another function (OrderBy
).
Hopefully, this guide accomplishes the following two objectives:
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.
To learn more about writing functional code in C#, watch Functional Programming with C# by Dave Fancher.