It is always nice to learn something new, regardless of our experience. In this short but very cool post, we’ll learn more about a little known feature having to do with Action
, Func
, and Predicate
in .NET.
A delegate
is a type that represents a reference to a method with a particular set of parameters and return type. Delegates are one of the fundamental building blocks of the .NET framework, and they received a significant upgrade with the introduction of the Language Integrated Query (LINQ) syntax. You may also here delegates referred to as Lambda Expressions.
There are three recognized Lambda expressions: Actions, Funcs, and Predicates.
An Action
is an expression that takes no parameters but executes a statement.
Action hello = () => Console.WriteLine("hello");
A Func
is an expression that can take any number of parameters, including no parameters, and must return a result.
Func<int, int> plusOne = (i) => i + 1;
A Predicate
is a specific kind of construct similar to Func
that takes in one of parameter and returns a bool
result.
Predicate<string> isItCake =
s =>
{
Console.WriteLine($"Checking {s} with \"cake\"");
return s?.Contains("cake", StringComparison.OrdinalIgnoreCase) == true;
};
As mentioned before, all of these lambda expressions inherit their behavior from the delegate
type.
Chaining Behaviors
Delegates allow developers to add as many handlers to a delegate
instance utilizing the +=
operator. Let’s walk through the invocation behavior of each type.
Action Chain Behavior
With an Action
, we can chain any number of actions after the first assignment.
Action Hello;
Hello = () => Console.WriteLine("Hello");
Hello += () => Console.WriteLine("World");
Hello += () => Console.WriteLine(".NET & Khalid\n");
Hello();
Executing this code yields the following results.
Hello
World
.NET & Khalid
As we can see, the app executes the actions based on their registration.
Func Chain Behavior
A Func
behaves slightly differently, as it could return a result. Let’s look at an example.
Func<string> groceries;
groceries = () =>
{
Console.WriteLine("1 Potato");
return "1 Potato";
};
groceries += () =>
{
Console.WriteLine("2 Apples");
return "2 Apples";
};
groceries += () =>
{
Console.WriteLine("3 Bagels");
return "3 Bagels";
};
// invoking groceries
var pick = groceries();
Console.WriteLine($"The pick is {pick}\n");
What should we expect the result to be? Well, we don’t have to guess.
1 Potato
2 Apples
3 Bagels
The pick is 3 Bagels
Executing the chained Func
will always return the last result. We could get the individual results by using the GetInvocationList
method and perform each Func
independently.
foreach (var @delegate in groceries.GetInvocationList())
{
var item = (Func<string>) @delegate;
Console.WriteLine($"purchasing: {item()}");
}
We now get each result for the individual Func
instances.
1 Potato
purchasing: 1 Potato
2 Apples
purchasing: 2 Apples
3 Bagels
purchasing : 3 Bagels
Predicate Chain Behavior
Like a Func
, Predicate
behave similarly, but need to be defined explicitly, meaning we can’t use var
here.
Predicate<string> isItCake =
s =>
{
Console.WriteLine($"Checking {s} with \"cake\"");
return s?.Contains("cake", StringComparison.OrdinalIgnoreCase) == true;
};
isItCake +=
s =>
{
Console.WriteLine($"Checking {s} with \"bread\"");
return s?.Contains("bread", StringComparison.OrdinalIgnoreCase) == true;
};
var bananaBread = "banana bread";
var result =
isItCake(bananaBread);
The result of invoking isItCake
yields a positive outcome.
Checking banana bread with "cake"
Checking banana bread with "bread"
Is banana bread cake? Yes
As we may have noticed, the result is that of the last registered Predicate
. We can use the GetInvocationList
method to iterate over each predicate instance.
foreach (var @delegate in isItCake.GetInvocationList())
{
var item = (Predicate<string>) @delegate;
Console.WriteLine($"is it Cake : {item(bananaBread)}");
}
Each delegate yields a different result.
Checking banana bread with "cake"
is it Cake : False
Checking banana bread with "bread"
is it Cake : True
Conclusion
Chaining is a powerful feature of .NET delegates and many developers may not realize that when passed an Action
, Func
, or Predicate
they may be getting more than one. When writing libraries that pass these types around, it may be necessary for library authors to check the GetInvocationList
and react accordingly: throw an exception, invoke each instance and aggregate the results, or do nothing different.
I hope you learned something new and exciting, and please leave a comment if you have any thoughts on the subject.