There, I did it! I dared to leave out covariance and contravariance from the title while that’s the very subject. Why? Because it isn’t important to know exactly what they mean and I still find those terms confusing. As a matter of fact, I won’t mention them again. If you want to know how they relate to the discussed subject, read Eric’s excellent description on the actual meaning of them. This is an introductory post to a few follow-up posts relating to the subject, so more is on the way!
You wouldn’t be reading this if you weren’t acquainted with inheritance, and probably you know about generics, so let’s start from there.
Step 1: Cats and dogs
Cats and dogs are animals.
class Animal { } class Dog : Animal { } class Cat : Animal { }
This allows you to safely do the following implicit casts, no big deal.
Cat cat = new Cat(); Dog dog = new Dog(); Animal animal = cat; animal = dog;
Step 2: Pets
Cats and dogs make excellent pets, so let’s use the following generic approach.
class Pet<T> where T : Animal { readonly T _animal; public Pet( T animal ) { _animal = animal; } public T RunAway() { return _animal; } public PlayWith( T friend ) { } }
Notice how pets can only play with animals of their own kind. Regardless of the cute picture above, this means cats and dogs don’t get along, and can’t play together. When a pet runs off, it is no longer a pet, but just an animal.
Pet<Dog> dog = new Pet<Dog>( new Dog() ); Pet<Cat> cat = new Pet<Cat>( new Cat() ); // Not possible! // dog.PlayWith( cat );
Of course, pet dogs can still play with dogs, and pet cats with cats.
Step 3: Stray pets
The delegate representation of a method returning an object and taking no arguments is Func. Pet.RunAway is exactly such a method, returning generic type T.
Pet<Dog> dog = new Pet<Dog>( new Dog() ); Func<Dog> runAway = dog.RunAway; Dog strayDog = runAway();
Programming is all about abstractions. When a pet runs off, is there always a need to know what kind of animal it is? The answer is no, any pet that runs off is certain to be an animal. You can easily do the following starting from .NET 4.0.
Func<Animal> runAwayDog = dog.RunAway; Func<Animal> runAwayCat = cat.RunAway; Animal strayAnimal = runAwayDog(); strayAnimal = runAwayCat(); Func<Animal> oldWay = () => dog.RunAway(); // Prior to .NET 4.0 this was possible using a lambda.
The same assumption can’t be made about the Pet.PlayWith method. I already demonstrated dogs can’t play with cats, so we can’t generalize dogs can play with any animal. So what is different about PlayWith? First, I need to introduce you to bulldogs.
Step 4: Bulldogs
class Bulldog : Dog { } ... var bulldogPet = new Pet<Dog>( new Bulldog() ); var dogPet = new Pet<Dog>( new Dog() ); // All dogs like each other ... dogPet.PlayWith( new Bulldog() ); bulldogPet.PlayWith( new Dog() );
A bulldog is a dog, but a dog isn’t always a bulldog. However, a bulldog is a dog, and any dog can play with any other dog, regardless of its kind. The following delegate casts are possible in .NET 4.0. Action represents a delegate with a single argument of type T but no return value.
Action<Dog> play = dog.PlayWith; Action<Bulldog> bullDogPlay = play; // If dogs can get along, bulldogs can get along as well. Action<Bulldog> oldAction = friend => play( friend ); // Prior to .NET 4.0 using a lambda.
Step 5: In and out
The conclusion is we can only make assumptions about types based on the interaction it is used in. Two interactions can be distinguished.
- Input: A type is used as input into the class. (e.g. function argument) The type can be the same type, or any extending type.
- Output: A type is returned from a class. (e.g. return value) The type should be the same type, meaning it is also any base type of that type.
The problem with generics is the type parameters can be used both as input and output, so an upcast to a ‘less’ generic type isn’t possible.
Pet<Dog> dog = new Pet<Dog>( new Dog() ); Pet<Cat> cat = new Pet<Cat>( new Cat() ); // Not possible ... // Pet<Animal> dogAnimal = dog; // Pet<Animal> catAnimal = cat; // dogAnimal.PlayWith( catAnimal ); // Dogs don't play with cats!
But, what if you can guarantee a template argument will only be used as input, or will only be used as output? .NET 4.0 allows you to indicate this on generic interfaces and delegates by using the in and out keywords. It is these keywords which allow the aforementioned casts of the Action and Func delegates. A decision was made to leave this feature out for generic classes, since there are little useful scenarios for it.
As an example for our pets, the following is an interface where the generic type is only used as output.
interface IPet<out T> { T RunAway(); } class Pet<T> : IPet<T> where T : Animal { ... } ... IPet<Dog> dog = new Pet<Dog>( new Dog() ); IPet<Animal> animal = dog; // The generic interface can be upcasted!
Is this still not flexible enough for your purposes? Be sure to follow my next few posts which will discuss useful workarounds in scenarios where more assumptions can be made, more specifically during reflection.
One thought on “Casting generic types”