One of the things I like about C#'s delegate types, compared to similar concepts in other languages, is that C#'s take on the concept lets you give a useful name to a family of functions:
With this definition, all functions that turn one thing into another thing are Converters. Nice.
We also have some generically-named
Func<…> delegate types defined for us, and these have become ubiquitous ever since LINQ was introduced. They're not as communicative, but still extremely useful, and for the most part the naming stays out of the way.
Delegate types allow you to give a name to a family of functions. However, you can run into trouble when you use two different names to refer to the same family of functions:
Since all methods are compatable with any delegate type that has matching input/output types, why aren't the following queries equivalent?
This was a little frustrating when C# 3 came out. Before then, all the delegate types we were used to working with had explicit names like
Predicate. When C# 3 introduced lambdas and LINQ, we gained
Action<…>, and I found myself stumbling while converting older code to use the new LINQ extension methods like
Select, which takes a
It's extra-frustrating because every single Converter<T, TResult> method that ever lived is also a Func<T, TResult>! I know it, you know it, Grandma tweeted it. We can drive that point home by making the
convert delegate work with
Select by wrapping it in a trivial do-nothing lambda:
integers.Select(x => convert(x)); Yeesh!
To help make sense of this behavior, we can make an analogy between delegates and interfaces.
Think of a delegate type as a special kind of interface type. The interface has only one method, called "Call". Since it is always called "Call", you can shorten the reader-unfriendly
convert.Call(5) to simply
Think of a method as an instance of these interfaces. All methods implement the infinite number of possible delegate 'interfaces' that match their input and output types.
Stringify is an object that implements the imaginary
IConverter<int, string> interface, and also implements the imaginary
IFunc<int, string> interface. Both of these interfaces just happen to contain the imaginary method with the signature:
string Call(int x).
Now, the example above makes more sense. We have two variables declared as 'interface' types, and assign the same object to both variables.
Select is written to accept instances of one 'interface' type, and although
convert points at a method compatable with
convert itself can only be seen as having some other, incompatible type.