August 20th, 2023
Do not relegate the delegate
If you are a software engineer, you have probably defined a lot of classes and interfaces. You will likely have also defined structs and enums. If you write in modern C# or Java then perhaps you have even played around with record types. These are all data constructs, but there are other things you can define. Depending on the kind of projects you work on, you are either intimately familiar with events or just see them once in a blue moon. But how many times have you defined a delegate?
I will now be talking specifically about .NET. Before the introduction of Action
and Func
in .NET 3.5, delegates were more common. Some of the older technology from .NET framework uses them a lot, like ADO.NET. They are also still common for the boilerplate event handlers in WPF and WinForms, even though those can be replaced by Actions
or Funcs
.
In fact, a lot of existing delegates can be replaced by Funcs
, like Predicate<T>
which is equivalent to Func<T, bool>
. The former is semantically clearer, but unfortunately the extension methods from Linq do not accept these older delegates (and the compiler will get confused if you try to make such a method yourself). Should Enumerable.Where
have taken a Predicate<T>
instead of Func<T, bool>
? Arguments can be made for both sides. I'll leave that to you.
Instead, in this article I would like to show you that there are still some cool delegates you can make that can be useful in various situations.
TryParse<T>
Parsing in object-oriented languages usually refers to translating a string value to a value of a different type. Most of the built-in structs of .NET define static Parse
and TryParse
methods. The difference between the two is that the former will throw if unable to parse, while the latter instead returns a boolean value that indicates whether the parse was succesful. The actual parsed value in a TryParse
is returned in an out
parameter, and that is where we see something not possible in a Func
.
public delegate bool TryParse<T>(string value, out T result);
With this new delegate, we can write a generic method that processes parsing. An example use case is implementing the decorator pattern where we want to write to a log everytime someone parses a string, so that over the lifetime of our application we can keep track of all values that have been parsed.
public bool TryParseAndLog<T>(string source, TryParse<T> tryParse, out T result) { // write source to log ... return tryParse(source, out result); }
Using this method would look like this:
TryParseAndLog<int>("123", int.TryParse, out var intResult); TryParseAndLog<double>("1.23", double.TryParse, out var doubleResult);
Note how the existing TryParse
methods from the built-in types fit perfectly.
TryGet<TSource, TOut>
If you break apart TryParse<T>
, you will find that it is essentially a TryGet<TSource, TOut>
with the TSource
preset to string. There are plenty of cases where the source will not be a string, so it should be worth defining this as another delegate.
public delegate bool TryGet<TSource, TOut>(TSource source, out TOut value);
To show the usefulness of this delegate, let me sketch the scenario. We have a sequence of keys, and we have a dictionary that may or may not contain items mapped to those keys. We want to iterate over the keys, finding only the ones that exist in the dictionary, and process the corresponding items.
Using Enumerable.Where
from Linq will not suffice here, because it does not translate the keys to mapped values. Likewise, Enumerable.Select
will not work because it does not filter to only those keys that are in the dictionary. Without this delegate, we would have to write a foreach loop, call TryGetValue
on the dictionary, and use continue if the key is not in the dictionary. However, there are times where a foreach loop is not viable, perhaps because your method is passed an enumerable that should not be exhausted.
To solve this, let us define our own enumerable extension method that uses this new delegate.
public static IEnumerable<(TSource Source, TOut Out)> WhereOut<TSource, TOut>( this IEnumerable<TSource> source, TryGet<TSource, TOut> tryGet) { var enumerator = source.GetEnumerator(); while (enumerator.MoveNext()) { var current = enumerator.Current; if (tryGet(current, out var value)) { yield return (current, value); } } }
Here I am preserving the key in the returned tuples, but depending on your use case, you may only need to return the mapped item. With this method defined, we can loop through our keys elegantly. In the following example, I am using the Each
extension method defined in an earlier article.
someKeys .WhereOut<TKey, TItem>(dictionary.TryGetValue) .Each(tuple => { var (key, item) = tuple; ... });
Unfortunately, we must still specify TKey
and TValue
when calling WhereOut
, because the compiler is not smart enough to infer them. Nevertheless, the code is cleaner than before.
AssessChange<T>
This delegate represents a function that takes two values that embody a change - the before and the after. Feel free to give this a different name, naming is hard after all.
public delegate void AssessChange<T>(T before, T after);
Unlike the previous examples, this does not use any special keywords like out
, and could therefore be replaced by an Action
. So why use it? The reason is that we can define the parameter names, thereby clarifying which of the two parameters is before and which is after. I can use named parameters to showcase this idea:
void Foo(AssessChange<int> assessChange) { assessChange(before: 1, after: 2); }
I would use a delegate such as this one solely to make my code easier to understand and maintain. If I am reviewing code where someone is invoking some Action<T, T>
to react to a change, I would have to spend additional time delving into the code to figure out which of the action's two parameters is before and which is after. Comments and documentation in code are important and useful, but far superior is expressing intent through clear names and types. When it comes to understanding (necessary for maintenance and implementation), the less you have to read, the better.