Revising C# Generics - dummies

By John Paul Mueller, Bill Sempf, Chuck Sphar

The generics model implemented in C# 2.0 was incomplete. Generics are fine for making the programmer’s life easier, but they did little in that version to make the analyst’s life easier. It used to be very hard to model an actual business model using Generics, and that changed in C# 4.0. Although parameters in C# 2.0 all allowed for variance in several directions, generics did 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 contravariant. 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 contravariant to string and integer return types (for example):

object theMessage = MethodThatGetsTheMessage();

Generics are invariant in C# 2.0 and 3.0. This means that if Basket<apple> is of type Basket<fruit>, those Baskets are not interchangeable as strings and objects are in the preceding example.

Variance

If you look at a method like the following 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 you try to call that method 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);

}

it will fail. Generics are invariant in C# 3.0. But, in Visual Studio 2010 and later this complies because IEnumerable<T> is covariant — you can use a more derived type as a substitute for a higher-order type.

Contravariance

A scheduling application could 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. One of these methods is MakeCalendar:

public void MakeCalendar(IEnumerable<Event> theEvents)

{

foreach (Event item in theEvents)

{

Console.WriteLine(item.WhenItIs.ToString());

}

}

Pretend that 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.

The application also has a sort algorithm at the main system called EventSorter that passes a sorted collection into the Sort method. It expects a call 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);

}

}

The event manager makes a list of courses, sorts them, and then makes a calendar. ScheduleCourses creates the list of courses and then calls courses.Sort() with EventSorter as an argument, as shown here:

public void ScheduleCourses()

{

List<Course> courses = new List<Course>()

{

new Course(){NumberOfStudents=20,

WhenItIs = new DateTime(2018,2,1)},

new Course(){NumberOfStudents=14,

WhenItIs = new DateTime(2018,3,1)},

new Course(){NumberOfStudents=24,

WhenItIs = new DateTime(2018,4,1)},

};

//Pass an ICompare<Event> class to the List<Course> collection.

//It should be an ICompare<Course>, but it can use ICompare<Event>

// because of contravariance

courses.Sort(new EventSorter());

//Pass 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 that calls Sort(), 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 it’s still possible to use the algorithm.

Now the application passes a list into the MakeSchedule method, but that method expects an enumerable collection of Events. Because parameters are covariant for generics now, it’s possible to pass in a List of courses, as Course is covariant to Event.

There is another example of contravariance, using parameters rather than return values. If you have a method that returns a generic list of courses, you 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’re working up the chain for parameters or down the chain for return types, C# will just magically figure the type out.

Covariance

The application now passes the list into the MakeSchedule method, but that method expects an enumerable collection of Events. Because parameters are covariant for generics now, it’s possible to pass in a List of courses, as Course is covariant to Event. This is covariance for parameters.