C#/.NET Little Wonders: The Predicate, Comparison, and Converter Generic Delegates
- by James Michael Hare
Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here. In the last three weeks, we examined the Action family of delegates (and delegates in general), the Func family of delegates, and the EventHandler family of delegates and how they can be used to support generic, reusable algorithms and classes. This week I will be completing my series on the generic delegates in the .NET Framework with a discussion of three more, somewhat less used, generic delegates: Predicate<T>, Comparison<T>, and Converter<TInput, TOutput>. These are older generic delegates that were introduced in .NET 2.0, mostly for use in the Array and List<T> classes. Though older, it’s good to have an understanding of them and their intended purpose. In addition, you can feel free to use them yourself, though obviously you can also use the equivalents from the Func family of delegates instead. Predicate<T> – delegate for determining matches The Predicate<T> delegate was a very early delegate developed in the .NET 2.0 Framework to determine if an item was a match for some condition in a List<T> or T[]. The methods that tend to use the Predicate<T> include: Find(), FindAll(), FindLast() Uses the Predicate<T> delegate to finds items, in a list/array of type T, that matches the given predicate. FindIndex(), FindLastIndex() Uses the Predicate<T> delegate to find the index of an item, of in a list/array of type T, that matches the given predicate. The signature of the Predicate<T> delegate (ignoring variance for the moment) is: 1: public delegate bool Predicate<T>(T obj);
So, this is a delegate type that supports any method taking an item of type T and returning bool. In addition, there is a semantic understanding that this predicate is supposed to be examining the item supplied to see if it matches a given criteria.
1: // finds first even number (2)
2: var firstEven = Array.Find(numbers, n => (n % 2) == 0);
3:
4: // finds all odd numbers (1, 3, 5, 7, 9)
5: var allEvens = Array.FindAll(numbers, n => (n % 2) == 1);
6:
7: // find index of first multiple of 5 (4)
8: var firstFiveMultiplePos = Array.FindIndex(numbers, n => (n % 5) == 0);
This delegate has typically been succeeded in LINQ by the more general Func family, so that Predicate<T> and Func<T, bool> are logically identical. Strictly speaking, though, they are different types, so a delegate reference of type Predicate<T> cannot be directly assigned to a delegate reference of type Func<T, bool>, though the same method can be assigned to both.
1: // SUCCESS: the same lambda can be assigned to either
2: Predicate<DateTime> isSameDayPred = dt => dt.Date == DateTime.Today;
3: Func<DateTime, bool> isSameDayFunc = dt => dt.Date == DateTime.Today;
4:
5: // ERROR: once they are assigned to a delegate type, they are strongly
6: // typed and cannot be directly assigned to other delegate types.
7: isSameDayPred = isSameDayFunc;
When you assign a method to a delegate, all that is required is that the signature matches. This is why the same method can be assigned to either delegate type since their signatures are the same. However, once the method has been assigned to a delegate type, it is now a strongly-typed reference to that delegate type, and it cannot be assigned to a different delegate type (beyond the bounds of variance depending on Framework version, of course).
Comparison<T> – delegate for determining order
Just as the Predicate<T> generic delegate was birthed to give Array and List<T> the ability to perform type-safe matching, the Comparison<T> was birthed to give them the ability to perform type-safe ordering.
The Comparison<T> is used in Array and List<T> for:
Sort()
A form of the Sort() method that takes a comparison delegate; this is an alternate way to custom sort a list/array from having to define custom IComparer<T> classes.
The signature for the Comparison<T> delegate looks like (without variance):
1: public delegate int Comparison<T>(T lhs, T rhs);
The goal of this delegate is to compare the left-hand-side to the right-hand-side and return a negative number if the lhs < rhs, zero if they are equal, and a positive number if the lhs > rhs. Generally speaking, null is considered to be the smallest value of any reference type, so null should always be less than non-null, and two null values should be considered equal.
In most sort/ordering methods, you must specify an IComparer<T> if you want to do custom sorting/ordering. The Array and List<T> types, however, also allow for an alternative Comparison<T> delegate to be used instead, essentially, this lets you perform the custom sort without having to have the custom IComparer<T> class defined.
It should be noted, however, that the LINQ OrderBy(), and ThenBy() family of methods do not support the Comparison<T> delegate (though one could easily add their own extension methods to create one, or create an IComparer() factory class that generates one from a Comparison<T>).
So, given this delegate, we could use it to perform easy sorts on an Array or List<T> based on custom fields. Say for example we have a data class called Employee with some basic employee information:
1: public sealed class Employee
2: {
3: public string Name { get; set; }
4: public int Id { get; set; }
5: public double Salary { get; set; }
6: }
And say we had a List<Employee> that contained data, such as:
1: var employees = new List<Employee>
2: {
3: new Employee { Name = "John Smith", Id = 2, Salary = 37000.0 },
4: new Employee { Name = "Jane Doe", Id = 1, Salary = 57000.0 },
5: new Employee { Name = "John Doe", Id = 5, Salary = 60000.0 },
6: new Employee { Name = "Jane Smith", Id = 3, Salary = 59000.0 }
7: };
Now, using the Comparison<T> delegate form of Sort() on the List<Employee>, we can sort our list many ways:
1: // sort based on employee ID
2: employees.Sort((lhs, rhs) => Comparer<int>.Default.Compare(lhs.Id, rhs.Id));
3:
4: // sort based on employee name
5: employees.Sort((lhs, rhs) => string.Compare(lhs.Name, rhs.Name));
6:
7: // sort based on salary, descending (note switched lhs/rhs order for descending)
8: employees.Sort((lhs, rhs) => Comparer<double>.Default.Compare(rhs.Salary, lhs.Salary));
So again, you could use this older delegate, which has a lot of logical meaning to it’s name, or use a generic delegate such as Func<T, T, int> to implement the same sort of behavior. All this said, one of the reasons, in my opinion, that Comparison<T> isn’t used too often is that it tends to need complex lambdas, and the LINQ ability to order based on projections is much easier to use, though the Array and List<T> sorts tend to be more efficient if you want to perform in-place ordering.
Converter<TInput, TOutput> – delegate to convert elements
The Converter<TInput, TOutput> delegate is used by the Array and List<T> delegate to specify how to convert elements from an array/list of one type (TInput) to another type (TOutput).
It is used in an array/list for:
ConvertAll()
Converts all elements from a List<TInput> / TInput[] to a new List<TOutput> / TOutput[].
The delegate signature for Converter<TInput, TOutput> is very straightforward (ignoring variance):
1: public delegate TOutput Converter<TInput, TOutput>(TInput input);
So, this delegate’s job is to taken an input item (of type TInput) and convert it to a return result (of type TOutput). Again, this is logically equivalent to a newer Func delegate with a signature of Func<TInput, TOutput>. In fact, the latter is how the LINQ conversion methods are defined.
So, we could use the ConvertAll() syntax to convert a List<T> or T[] to different types, such as:
1: // get a list of just employee IDs
2: var empIds = employees.ConvertAll(emp => emp.Id);
3:
4: // get a list of all emp salaries, as int instead of double:
5: var empSalaries = employees.ConvertAll(emp => (int)emp.Salary);
Note that the expressions above are logically equivalent to using LINQ’s Select() method, which gives you a lot more power:
1: // get a list of just employee IDs
2: var empIds = employees.Select(emp => emp.Id).ToList();
3:
4: // get a list of all emp salaries, as int instead of double:
5: var empSalaries = employees.Select(emp => (int)emp.Salary).ToList();
The only difference with using LINQ is that many of the methods (including Select()) are deferred execution, which means that often times they will not perform the conversion for an item until it is requested.
This has both pros and cons in that you gain the benefit of not performing work until it is actually needed, but on the flip side if you want the results now, there is overhead in the behind-the-scenes work that support deferred execution (it’s supported by the yield return / yield break keywords in C# which define iterators that maintain current state information).
In general, the new LINQ syntax is preferred, but the older Array and List<T> ConvertAll() methods are still around, as is the Converter<TInput, TOutput> delegate.
Sidebar: Variance support update in .NET 4.0
Just like our descriptions of Func and Action, these three early generic delegates also support more variance in assignment as of .NET 4.0.
Their new signatures are:
1: // comparison is contravariant on type being compared
2: public delegate int Comparison<in T>(T lhs, T rhs);
3:
4: // converter is contravariant on input and covariant on output
5: public delegate TOutput Contravariant<in TInput, out TOutput>(TInput input);
6:
7: // predicate is contravariant on input
8: public delegate bool Predicate<in T>(T obj);
Thus these delegates can now be assigned to delegates allowing for contravariance (going to a more derived type) or covariance (going to a less derived type) based on whether the parameters are input or output, respectively.
Summary
Today, we wrapped up our generic delegates discussion by looking at three lesser-used delegates: Predicate<T>, Comparison<T>, and Converter<TInput, TOutput>.
All three of these tend to be replaced by their more generic Func equivalents in LINQ, but that doesn’t mean you shouldn’t understand what they do or can’t use them for your own code, as they do contain semantic meanings in their names that sometimes get lost in the more generic Func name.
Tweet
Technorati Tags: C#,CSharp,.NET,Little Wonders,delegates,generics,Predicate,Converter,Comparison