Understanding variance
Working with contravariance
Using covariance
Generics are covered in length in Books I and II, as they relate to creating collections of objects or business concepts, and how they impact object-oriented programming. They also play a large role in dynamic design and programming, which Chapter 1 of this book covers.
The generics model implemented in C# 2.0 was incomplete. Although parameters in C# all allow for variance in several directions, generics do not.
Variance has to do with types of parameters and return values. Covariance means that an instance of a subclass can be used when an instance of a parent class is expected, while Contravariance means that an instance of a superclass can be used when an instance of a subclass is expected. When neither is possible, it is called Invariance.
All fourth-generation languages support some kind of variance. In C# 3.0 and earlier versions, parameters are covariant and return types are contravarient. So, this works because string and integer parameters are covariant to object parameters:
public static void MessageToYou(object theMessage) { if (theMessage != null) Console.Writeline(theMessage) } //then: MessageToYou("It's a message, yay!"); MessageToYou(4+6.6);
And this works because object return types are contravarient to string and integer return types (for example):
object theMessage = MethodThatGetsTheMessage();
Generics are nonvariant in C# 2.0 and 3.0. This means that if Basket<apple>
is of type Basket<fruit>
, those Baskets
are not interchangeable like strings and objects are in the preceding example.
If we look at a method like the preceding one:
public static void WriteMessages() { List<string> someMessages = new List<string>(); someMessages.Add("The first message"); someMessages.Add("The second message"); MessagesToYou(someMessages); }
and then we try to call that method like we did earlier with a string type:
//This doesn't work in C#3!! public static void MessagesToYou(IEnumerable<object> theMessages) { foreach (var item in theMessages) Console.WriteLine(item); }
this fails in Visual Studio 2008. Generics are invariant in C# 3.0. But, in Visual Studio 2010 this complies because IEnumerable<T>
is covariant — you can use a more derived type as a substitute for a higher-order type. Let's look at a real example.
In my scheduling application, I have Events
, which have a date, and then a set of subclasses, one of which is Course
. A Course
is an Event
. Courses know their own number of students.
Anyway, back at the ranch, I have a method called MakeCalendar
.
public void MakeCalendar(IEnumerable<Event> theEvents) { foreach (Event item in theEvents) { Console.WriteLine(item.WhenItIs.ToString()); } }
Pretend it makes a calendar; for now, all it does is print the date to the console. MakeCalendar
is systemwide, so it expects some enumerable list of events.
I also have a Sort algorithm at the main system, called EventSorter
. This is used to pass into the Sort
method of collections. It expects to be called from a list of Events. Here is the EventSorter
class:
class EventSorter : IComparer<Event> { public int Compare(Event x, Event y) { return x.WhenItIs.CompareTo(y.WhenItIs); } }
I am writing the Instructor Led Training section of the event manager, and I need to make a list of courses, sort them, and then make a calendar. So I make my list of courses in ScheduleCourses
, then I call sort and pass in the EventSorter:
public void ScheduleCourses() { List<Course> courses = new List<Course>() { new Course(){NumberOfStudents=20, WhenItIs = new DateTime(2009,2,1)}, new Course(){NumberOfStudents=14, WhenItIs = new DateTime(2009,3,1)}, new Course(){NumberOfStudents=24, WhenItIs = new DateTime(2009,4,1)}, }; //Now I am passing an ICompare<Event> class to my List<Course> collection. //It should be an ICompare<Course> but I can use ICompare<Event> because of contravariance courses.Sort(new EventSorter()); //I am passing a List of courses, where a List of Events was expected. //We can do this because generic parameters are covariant MakeCalendar(courses); }
But wait, this is a list of courses I am calling Sort from, right, not a list of events. Doesn't matter — IComparer<Event>
is a contravariant generic for T (its return type) as compared to IComparer<Course>
so I can still use the algorithm.
Now I have to pass my list into the MakeSchedule
method, but that method expects an enumerable collection of Events
. Because parameters are covariant for generics now, I can pass in a List of Courses, as Course
is covariant to Event.
Make sense?
There is another example of contravariance, using parameters rather than return values. If I have a method that returns a generic list of Courses
, I can call that method expecting a list of Events
, because Event
is a superclass of Course
.
You know how you can have a method that returns a String
and assign the return value to a variable that you have declared an object? Now you can do that with a generic collection, too.
In general, the C# compiler makes assumptions about the generic type conversion. As long as you are working up the chain for parameters, or down the chain for return types, C# will just magically figure the type out.