Invariance, Covariance and Contravariance in C#

Your Ad Here

Invariance, Covariance and Contravariance Explained

Let's examine what invariant, covariant, and contravariant parameters and return types mean. You almost certainly are familiar with the terms, even if you don't have a grasp of the formal definitions.

A return value or parameter is invariant if you must use the exact match of the formal type name. A parameter is covariant if you can use a more derived type as a substitute for the formal parameter type. A return value is contravariant if you can assign the return type to a variable of a less derived type than the formal parameter.

In most cases, C# supports covariant parameters and contravariant return types. That's consistent with almost every other object-oriented language. In fact, polymorphism is usually built around the concepts of covariance and contravariance. You intuitively know that you can pass a derived class object to any method expecting a base class object. You intuitively know that you can pass a derived object to any method expecting a base object. After all, the derived object is also an instance of the base object. You instinctively know that you can store the result of a method in a variable of a less-derived object type than the formal method return type.

For example, you expect this to compile:

public static void PrintOutput(object thing)
{
    if (thing != null)
        Console.WriteLine(thing);
}
// elsewhere:
PrintOutput(5);
PrintOutput("This is a string");

This works because parameter types are covariant in C#. Similarly, you can store the result of any method in a variable of type object, because return types in C# are contravariant:

object value = SomeMethod();

If you've done any work with C# or Visual Basic.NET since .NET was first released, all this is familiar ground. However, the rules change as you begin to look at any type that represents a collection of objects. In too many ways, what you intuitively think should work just doesn't. As you dive deeper, you may find that what you believe is a bug is actually the language specification. Now it's time to explain why collections work differently, and what's changing in the future.

Object-Based Collections


The .NET 1.x collections (ArrayList, HashTable, Queue and so on) could be treated as covariant. Unfortunately, they're not safely covariant. In fact, they're invariant. However, because they store references to System.Object, they appear to be covariant and contravariant. A few examples quickly illustrate the issue.

You could believe these collections act as covariant because you can create an ArrayList of Employee objects and use that as a parameter to any method that uses objects of type ArrayList. Often, that approach works just fine. This method would work with any arraylist:

private void SafeCovariance(ArrayList bunchOfItems)
{
    foreach(object o in bunchOfItems)
        Console.WriteLine(o);
    // reverse the items:
   int start = 0;
   int end = bunchOfItems.Count - 1;
    while (start <>
    {
        object tmp = bunchOfItems[start];
        bunchOfItems[start] = bunchOfItems[end];
        bunchOfItems[end] = tmp;
        start++;
        end--;
    }
 
    foreach(object o in bunchOfItems)
        Console.WriteLine(o);
}

This method is safe because it doesn't change the type of any object in the collection. It enumerates the collection, and it moves items already in the collection to different indices in the collection. However, none of the types change, so this method will work in all instances.

However, ArrayList, and other classic .NET 1.x collections cannot be considered safely covariant. Look at this method:

private void UnsafeUse(ArrayList stuff)
{
    for (int index = 0; index <>
        stuff[index] = stuff[index].ToString();
}

It's making deeper assumptions about the objects stored in the collection. After the method exits, the collection contains objects of type string. That may not have been the type of the original collection. In fact, if the original collection contained strings, the method does nothing. Otherwise, it transforms the collection into a different type.

The following usage example shows the kinds of problems encountered when you call this method. Here, a list of numbers is sent to UnsafeUse, where it's transformed into an ArrayList of strings. After that call, the calling code tries again to create the sum of the items, which now causes an InvalidCastException.

// usage:
public void DoTest()
{
    ArrayList collection = new ArrayList()
    {
        1,2,3,4,5,6,7, 8, 9, 10,
        11,12,13,14,15,16,17,18,19,20,
        21,22,23,24,25,26,27,28,29,30
    };
 
 
     SafeCovariance(collection);
     // create the sum:
     int sum = 0;
     foreach (int num in collection)
         sum += num;
     Console.WriteLine(sum);
 
     UnsafeUse(collection);
    // create the sum:
    sum = 0;
    try
    {
        foreach (int num in collection)
            sum += num;
        Console.WriteLine(sum);
    }
    catch (InvalidCastException)
    {
        Console.WriteLine(
            "Not safely covariant");
    }
}

This example shows that while classic collections are invariant, you could, for all practical purposes, treat them as though they were covariant (or contravariant). But these collections are not safely covariant. The compiler does nothing to keep you from making mistakes in how you treat the objects in a classic collection.

Arrays
When used as a parameter, arrays are sometimes invariant, and sometimes covariant. Once again, just like the classic collections, arrays are not safely covariant.

First and foremost, only arrays containing reference types can be treated as either covariant or contravariant. Arrays of value types are always invariant. That's true even when trying to call a method that expects an object array. This method can be called with any array of reference types, but you cannot pass it an array of integers or any other value type:

private void PrintCollection(object[] collection)
{
    foreach (object o in collection)
        Console.WriteLine(o);
}

As long as you constrain yourself to reference types, arrays are covariant and contravariant. However, they're not safely covariant or safely contravariant. The more often you treat arrays as covariant or contravariant, the more you'll find that you need to handle ArrayTypeMismatchException. Let's examine some of the ways.

Array parameters are covariant, but not safely covariant. Examine this dangerous method:

private class B
{
    public override string ToString()
    {
        return "This is a B";
    }
}
 
private class D : B
{
    public override string ToString()
    {
        return "This is a D";
    }
}
 
private class D2 : B
{
    public override string ToString()
    {
        return "This is a D2";
    }
}
 private void DestroyCollection(B[] storage)
{
    try
    {
        for (int index = 0; index <>
            storage[index] = new D2();
    }
    catch (ArrayTypeMismatchException)
    {
        Console.WriteLine("ArrayTypeMismatch");
    }
}

The following calling sequence will cause the loop to throw an ArrayTypeMismatch exception:

D[] array = new D[]{
    new D(),
    new D(),
    new D(),
    new D(),
    new D(),
    new D(),
    new D(),
    new D(),
    new D(),
    new D()};
 
DestroyCollection(array);

The reason is obvious when you see the two blocks together. The call site created an array of D objects, then calls a method that expects an array of B objects. Because arrays are covariant, you can pass the D[] to the method expecting B[]. But, inside DestroyCollection(), the array can be modified. In this case, it creates new objects for the collection, objects of type D2. That's fine in the context of that method: D2 objects can be stored in a B[] because D2 is derived from B. But, the combination often causes errors.

The same thing happens when you introduce some method that returns the array storage and treat that as a contravariant value. This code looks like it would work fine:

B[] storage = GenerateCollection();
storage[0] = new B();

However, if the body of GenerateCollection looks like this, it will cause an ArrayTypeMismatch exception when the storage[0] element is set to a B object.

Generic Collections
Arrays suffer from being treated as covariant and contravariant, even when that's not safe. The .NET 1.x collection types are invariant, but stored references to System.Object, which wasn't type safe in any practical sense. The generic collections in .NET 2.x and beyond suffer from being invariant. That means you cannot ever substitute a collection containing a more derived object type where a collection containing a less derived type is expected. That's a lengthy way to say that a lot of substitutions you expect to work don't. You'd think that you could write a method like this:

private void WriteItems(IEnumerable sequence)
{
    foreach (var item in sequence)
        Console.WriteLine(item);
}

You'd think you could call it with any collection that implements IEnumerable because any T must derive from object. That may be your expectation, but because generics are invariant, the following will not compile:

IEnumerable items = Enumerable.Range(1, 50);
WriteItems(items); // generates CS1502, CS1503

You can't treat generic collection types as contravariant, either. This line will not compile because you cannot convert IEnumerable into IEnumerable when assigning the return value:

IEnumerable moreItems =
     Enumerable.Range(1, 50);

You might think IEnumerable derives from IEnumerable, but it doesn't. IEnumerable is a Closed Generic Type based on the Generic Type Definition for IEnumerable. IEnumerable is another Closed Generic Type based on the Generic Type Definition for IEnumerable. One does not derive from the other. There's no inheritance relationship, and you cannot treat them as covariant. Even though there's an inheritance relationship between the two type parameters (int, and object), there's no corresponding inheritance relationship between any generic types using those type parameters. It feels like it should work, and strictly speaking, it does work correctly.

C# treating generics invariantly has some very powerful advantages. Most significantly, you can't make the mistakes I demonstrated earlier with arrays and the 1.x style collections. Once you get generic code to compile, you've got a much better chance of making it work correctly. That's consistent with C#'s heritage as a strongly typed language that leverages the compiler to remove possible bugs in your code.

However, that heavy reliance on strong typing feels restrictive. Both of the constructs I just showed for generic conversions feel like they should just work. And yet, you don't want to revert to the same behavior used for the .NET 1.x collections and arrays. What we really want is to treat generic types as covariant or contravariant only when it works, not when it simply substitutes a runtime error for a compile time error.


Source:http://reddevnews.com

Subscribe
Posted in |

3 comments:

  1. Anonymous Says:

    I noiselessness can’t think of the time I went to France and Netherlands, it was my best holiday ever. Western Europe countries are titanic for tourists, as there are surprising attractions, picturesque landscapes and you can have angelic making whoopee there. The prices could be lower, but pacific is worth seeing these places. Check outdoors: [url=http://www.transport-warszawa.info/]ship warszawa[/url]

  2. Anonymous Says:

    Just thought I should take a second and say howdy to everybody. Looking forward to your forum and what all the memebers has to say. I've been spending to much time on http://kids.yahoo.com/games . Needed a break.

  3. Anonymous Says:

    Just wanted to take a moment and say what's up to everyone. Looking forward to the site and what all the memebers has to talk about. I've been spending to much time on http://kids.yahoo.com/games . Needed a break.