Generic predicates are pretty powerful, but the FindAll implementation doesn't show it.
By a raise of hands, how many people love foreach? I'm starting to like it quite a bit, even though I often refactor it out of a loop because I need the index. That is probably the number one reason people actually perform the refactoring. However, when using any non enumeration syntax, you don't get the versioning concepts that are available to enumerators, and so if the collection changes you never know. This could be a big deal, or it might not, but the versioning is pretty neat nonetheless.
Being as I loved foreach, I figured they'd give it some love in Whidbey. I'm back to looking at List<T>. So if I grab an enumerator off of List<T> I can't pass it a Predicate<T>. Have you see predicates yet? They allow you to return a true/false as to whether or not something matches your criterion. So enumeration in a collection can't have predicates. Well, that kind of sucks, and is something I'd love to see added at the base level. Basically:
GetEnumerator(new Predicate<TypeICreatedCollectionWith>(MyPredicate));
Sweet. Now I can enumerat only those items that match my list, skipping over any that don't. That would be perfect, but it doesn't exist yet. So what does exist? Well, FindAll. This returns a List<T> based on your closed type. Keyword is that it creates a new collection. Not the best, but you can do it. I'll include a full sample as the end, but a FindAll call might look like the following:
List<Organism> organisms = new List<Organism>(30);
for(int i = 0; i < 10; i++) {
organisms.Add(new Plant());
organisms.Add(new SeaPlant());
organisms.Add(new LandPlant());
organisms.Add(new Carnivore());
organisms.Add(new Herbivore());
}
List<Organism> plants = organisms.FindAll(new Predicate<Organism>(PlantsOnlyPredicate));
The above PlantsOnlyPredicate obviously checks the type and does some work. Notice the problem with the above? My Predicate only returns Plants, however, List<T> is of open type Organism still. Even worse, you can't just cast it around like you used to be able to. So FindAll doesn't support mutation. Is there a method that does? Yep. ConvertAll. ConvertAll takes a Converter<T,V> and returns you a new List<V>. So now the code looks something like:
List<Organism> plants = organisms.FindAll(new Predicate<Organism>(PlantsOnlyPredicate));
List<Plant> castPlants = plants.ConvertAll(new Converter<Organism, Plant>(PlantConverter));
I think it is time these two got merged eh? What about taking a FindAll, that ALSO took a converter. You could write a method that returned the new strongly typed collection cast up if need be. That seems pretty cool right? Well, it could cast an exception if your converter can't make the cast, but that really is the programmers problem. To get back to the original problem, you now have to FindAll, ConvertAll, and then GetEnumerator to get an enumerator for only a specific sub-set of your collection (you can take out the ConvertAll if your predicate isn't doing work based on type, but instead based on some other criterion, but I remember users that wanted to enumerate over say a collection of controls, but only enumerate the buttons, say in a WinForms environment).
I have two solutions really. The first solution just does the conversion. It has to box the value (cast to object) in order to get around the compiler junk, but it does return your strongly typed array cast to whatever the ReturnType is. Check it out, but notice the casting doesn't look cool because we aren't using the indirection of a converter:
private static List<ReturnType> FindAllMutate<ReturnType, ListType>
(List<ListType> listToMutate, Predicate<ListType> predicate)
{
List<ReturnType> mutator = new List<ReturnType>();
foreach(ListType listItem in listToMutate) {
if ( predicate(listItem) ) {
mutator.Add((ReturnType) ((object) listItem));
}
}
return mutator;
}
Note, you can't really use constraints here. Why? Well, because think of how they work and get back to me if you find a way to use them. I couldn't find a way to do the conversion generically with a constraint since the conversion could be to a class derived from ListType, a class ListType is deriving from, or an interface ListType is implementing.
I think the Converter is pretty nice though. It basically forces you to write a method with the ListType as a parameter and an output of the ReturnType. C# will then worry about your casting later and as long as your Converter is written for the type you are converting, then the methods all work. Very nice, since now the compiler has more information. Rewritten with the converter the FindAllMutate now looks different.
private static List<ReturnType> FindAllMutate<ReturnType, ListType>
(List<ListType> listToMutate, Predicate<ListType> predicate, Converter<ListType, ReturnType> converter)
{
List<ReturnType> mutator = new List<ReturnType>();
foreach(ListType listItem in listToMutate) {
if ( predicate(listItem) ) {
mutator.Add(converter(listItem));
}
}
return mutator;
}
Well, no more sloppying casting. And I can write whatever I would like in my converter, including custom code that knows how to create the proper type even if there isn't an easy conversion.
That appears to accomplish my goals for this blog. I'd love to see some more powerful collection routines added to List at the base rather than writing them. That way I can be assured they'll be there as I'm writing code for customers. I'm not seeing any real generics support in the Windows Forms library, so my original problem of enumerating the Controls collection for only the controls matching a particular type might not be as close as I think. Here is some test code for predicates with a Terrarium theme, Enjoy!
using System;
using System.Collections;
using System.Collections.Generic;
public class Organism {}
public class Plant : Organism {}
public class Animal : Organism {}
public class Carnivore : Animal {}
public class Herbivore : Animal {}
public interface ISeaPlant {}
public interface ILandPlant {}
public class SeaPlant : Plant, ISeaPlant {}
public class LandPlant : Plant, ILandPlant {}
public class PredicateCategorizer {
private static void Main(string[] args) {
List<Organism> organisms = new List<Organism>(30);
for(int i = 0; i < 10; i++) {
organisms.Add(new Plant());
organisms.Add(new SeaPlant());
organisms.Add(new LandPlant());
organisms.Add(new Carnivore());
organisms.Add(new Herbivore());
}
List<Organism> plants = organisms.FindAll(new Predicate<Organism>(PlantsOnlyPredicate));
Console.WriteLine(plants.Count);
List<Organism> herbivores = organisms.FindAll(new Predicate<Organism>(HerbivoresOnlyPredicate));
Console.WriteLine(herbivores.Count);
List<Organism> carnivores = organisms.FindAll(new Predicate<Organism>(CarnivoresOnlyPredicate));
Console.WriteLine(carnivores.Count);
List<Plant> plantsPreCast = FindAllMutate<Plant, Organism>(organisms, new Predicate<Organism>(PlantsOnlyPredicate));
Console.WriteLine(plantsPreCast.Count);
List<ISeaPlant> plantsSeaOnly = FindAllMutate<ISeaPlant, Organism>(organisms, new Predicate<Organism>(SeaPlantsOnlyPredicate), new Converter<Organism, ISeaPlant>(SeaPlantConverter));
Console.WriteLine(plantsSeaOnly.Count);
}
private static Plant PlantsConverter(Organism input) {
return input as Plant;
}
private static ISeaPlant SeaPlantConverter(Organism input) {
return input as ISeaPlant;
}
private static bool PlantsOnlyPredicate(Organism underlying) {
if ( underlying is Plant ) { return true; } else { return false; }
}
private static bool SeaPlantsOnlyPredicate(Organism underlying) {
if ( underlying is ISeaPlant ) { return true; } else { return false; }
}
private static bool HerbivoresOnlyPredicate(Organism underlying) {
if ( underlying is Herbivore ) { return true; } else { return false; }
}
private static bool CarnivoresOnlyPredicate(Organism underlying) {
if ( underlying is Carnivore ) { return true; } else { return false; }
}
private static List<ReturnType> FindAllMutate<ReturnType, ListType>
(List<ListType> listToMutate, Predicate<ListType> predicate, Converter<ListType, ReturnType> converter)
{
List<ReturnType> mutator = new List<ReturnType>();
foreach(ListType listItem in listToMutate) {
if ( predicate(listItem) ) {
mutator.Add(converter(listItem));
}
}
return mutator;
}
private static List<ReturnType> FindAllMutate<ReturnType, ListType>
(List<ListType> listToMutate, Predicate<ListType> predicate)
{
List<ReturnType> mutator = new List<ReturnType>();
foreach(ListType listItem in listToMutate) {
if ( predicate(listItem) ) {
mutator.Add((ReturnType) ((object) listItem));
}
}
return mutator;
}
}