3.5. Variance

Variance has changed in .NET 4.0. At the 2008 PDC, Anders Hejlsberg (lead architect of C#) summarized the changes to variance as "[allowing] you to do things in your code that previously you were surprised you couldn't do."

For those who are already comfortable with the concept of variance (stop looking so smug), here is the short version of what has changed in .NET 4.0:

  • You can now mark parameters in generic interfaces and delegates with the out keyword to make them covariant, and with the in keyword to make them contravariant (In and Out in VB.NET).

  • The in/out keywords have been added to some commonly used generic interface and delegate types to now enable them to support safe co- and contravariance (e.g., IEnumerable<out t> and Action<in t>).

If this means nothing to you, read on. Variance is a confusing subject, and I suspect you can have a happy development career without it ever really affecting you. It is applicable only in rare situations.

Don't feel bad if you have never heard of variance. I hadn't either, before researching this chapter. At the WebDD09 conference in the United Kingdom, I asked who was familiar with variance in a room of 120 developers to find that just 3 people put their hand up.

3.5.1. The Long Version for Everyone Else

To really understand variance, we need to go back in time to somewhere around the year 2000. Was the world a happier place? Who knows. But what's important for our discussions is that Java was pretty big around that time, and Microsoft wanted to lure Java developers over to C#. To ease the transition between the two languages, Microsoft brought some controversial functionality from Java to .NET and made arrays covariant, which is bad for the following reasons.

3.5.1.1. Bad Arrays of Animals and Elephants

To demonstrate why covariant arrays are bad:

  1. Create a new console application called Chapter3.Variance.

  2. Now create two classes, Animal and Elephant. Note that Elephant inherits from Animal; this is important.

    public class Animal { }
    public class Elephant : Animal { }

  3. Enter the following code:

    class Program
    {
        public class Animal { }
        public class Elephant : Animal { }
    
        static void Main(string[] args)
        {
            Animal[] animals = new Elephant[10];
            animals[0] = new Animal();
        }
    }

  4. Compile your code, and everything should be fine. However, run the application, and an exception will be thrown on the second line:

    Attempted to access an element as a type incompatible with the array

Whoa, but the compiler let us do this, and then complains about a type exception. What gives?

  • We are allowed to put Elephants in an Animal array, because Elephant inherits from Animal and it will also have all the properties of Animal.

  • Animals, however, will not necessarily have features specific to Elephants (such as trunks, tusks, and an enviable memory), so the reverse is not true.

  • This exception occurs because our Animal array actually consists of Elephants, so it is essentially an Elephant array.

Code that compiles but throws type exceptions at runtime is bad news, so when generics were introduced, Microsoft made generics invariant. The following will not compile for the previously stated reasons:

List<Animal> Animals = new List<Animal>();
//This will work fine as we can be sure Elephant has all Animal's properties
Animals.Add(new Elephant());
List<Elephant> Elephants = new List<Elephant>();
//This will not compile
Elephants.Add(new Animal());

This is further illustrated in Figure 3-2.

Figure 3.2. Generic lists and variants, or why elephants are mean

3.5.1.2. So, What's the Problem?

Well, some folks found that .NET would stop them from writing code and modeling situations that are safe. For example, the following did not work prior to .NET 4.0:

IList<Elephant> elephants = new List<Elephant>();
IEnumerable<Animal> animals = elephants;

Here, the exception is

Cannot implicitly convert type
'System.Collections.Generic.IList<Chapter3.Elephant>' to
'System.Collections.Generic.IEnumerable<Chapter3.Animal>'. An explicit conversion
exists (are you missing a cast?)

There is no reason that this shouldn't work, as the code is safe. By using the IEnumerable interface, Animals are only ever returned in the output position, and you cannot do any reassignment. So the new variance changes are fixing a problem that should never have existed.

3.5.1.3. The out Keyword

The previous example works in .NET 4.0 because the IEnumerable<T> interface now has the out keyword in its parameter list, which enables us to use a more specific class (Elephant) when a more general class (Animal) should be used. The out keyword tells the compiler that Animal can only ever be returned (in the output position), which keeps the compiler happy, because IEnumerable contains no way for you to add objects to it after IEnumerable is declared. This avoids the problems discussed previously and ensures type safety.

The term for this is covariance, and it allows an item to be treated as its supertype. For example, IEnumerable<string> can also be IEnumerable<object> (note that variance only applies to reference types, so it will not affect integer, for example).

Contravariance, which we will look at shortly, is the exact opposite. It allows a type like Action<Object> to be treated as a subtype Action<String>.

You can add the out modifier to your own interfaces and delegates. It has also been added to the following generic intefaces:

  • IEnumerable<out T>

  • IEnumerator<out T>

  • IQueryable<out T>

3.5.2. Contravariance

Contravariance is the opposite of covariance, and allows you to use a more general class when a specific class should be used. Contravariance uses the in modifier to specify that the parameter can only occur in the input position. You can add the in modifier to your own interfaces and delegates, and it has been added to the following generic interfaces and delegates:

  • IComparer<in T>

  • IEqualityComparer <in T>

  • Func<in T,.., out R>

  • Action<in T,..>

  • Predicate<in T>

  • Comparison<in T>

  • EventHandler<in T>

3.5.2.1. Example of Contravariance

Let's say we want to create a common class that will sort animals by weight. It would be great if we could use this same class to weigh anything that had a base class of Animal, but as you can guess, the code we will write won't work in VS2008.

We will add Weight and Name properties to the Animal class so we can easily identify individual Animal instances.

  1. Modify the Animal class so that it looks like the following:

    public class Animal
    {
        public int Weight { get; set; }
        public string Name { get; set; }
    
        public Animal()
        { }
    
        public Animal(string InputName, int InputWeight)
        {
            Name = InputName;
            Weight = InputWeight;
        }
    }

  2. Now modify the Elephant class to the following:

    public class Elephant : Animal
    {
        public Elephant(string InputName, int InputWeight) :
          base(InputName, InputWeight)
        {
        }
    }

  3. To weigh all our animals, we will create a new class called WeightComparer that will implement the IComparer interface; implementing the IComparer interface will then enable us to use the Sort() method on the list object. Create the WeightComparer class with this code:

    public class WeightComparer : IComparer<Animal>
    {
    
        public int Compare(Animal x, Animal y)
        {
            if (x.Weight > y.Weight) return 1;
            if (x.Weight == y.Weight) return 0;
            return −1;
        }
    }

    ICOMPARER

    IComparer accepts two parameters and will return an integer representing whether one object is greater, equal, or less than the other. In our example, we return the following:

    • 0 if x.weight equals y.weight

    • 1 if x.weight is more than y.weight

    • −1 if x.weight is less than y.weight


  4. We will now add some animals to sort, so alter the Main() method to the following:

    static void Main(string[] args)
    {
        WeightComparer objAnimalComparer = new WeightComparer();
    
        List<Animal> Animals = new List<Animal>();
        Animals.Add(new Animal("elephant", 500));
        Animals.Add(new Animal("tiger", 100));
        Animals.Add(new Animal("rat", 5));
    
        //Works
        Animals.Sort(objAnimalComparer);
    
        List<Elephant> Elephants = new List<Elephant>();
        Elephants.Add(new Elephant("Nellie", 100));
        Elephants.Add(new Elephant("Dumbo", 200));
        Elephants.Add(new Elephant("Baba", 50));
    
        //Doesn't work prior to .NET 4
        Elephants.Sort(objAnimalComparer);
    
        Elephants.ForEach(e=> Console.WriteLine(e.Name + " " + e.Weight.ToString()));
    }

  5. Compile the code and you should find that the elephants are sorted by weight; however, if you try this code in VS2008, you will find it will not compile.

3.5.3. Further Reading

Variance is a difficult subject, so for more information please refer to the following blogs and book:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.229.172