What is the point behind this covariance related code?

Viewed 62

I have been reading a book named c# 7.0 in a Nutshell by O'REILLY, Topic: Variance is not Automatic. There is an example with 2 classes, Animal and Bear in which Animal>Bear:

public class Animal { }
public class Bear: Animal { }

And also there is a class like this:

public class Stack<T>
{
    private int position;
    T[] data = new T[100];
    public void Push(T obj) => data[position++] = obj;
    public T Pop() => data[--position];
}

In continue there are 2 versions of same class:

public class ZooCleaner1
{
    public static void Wash(Stack<Animal> animals) { }
}

and:

public class ZooCleaner2
{
    public static void Wash<T>(Stack<T> animals) where T: Animal { }
}

It explains that if I try to write:

ZooCleaner1.Wash(bears);
ZooCleaner2.Wash(bears);

the first line gets a compile-time error which says that it is not able to convert Bear to Animal. But the second line is correct and works fine. As I am new to this topc, I can't Understand the diffrence betwean these 2 lines and I think they both accept Stack<Animal> and why do we need to use conditional generics?

2 Answers

Stack<Animal> represents a stack of objects of any Animal type. Stack<T> where T: Animal represents a stack of a single type so long as that type inherits from Animal.

You cannot use a Stack<Bear> in place of a parameter declared as Stack<Animal> becuse, if you could, then the method could push a Fish onto the stack of bears. When the method that is using the stack of Bears pops it off of the stack, imagine the surprise when it pops off a fish!

The second method, on the other hand, is generic, meaning that it can accept a stack of any type, so long as that type inherits from Animal So if the method gets a Stack<Bear>, it can only push another Bear onto the stack. Trying to push a Fish would be a runtime error.

I wouldn't call this "covariance". This is generic variance. Your code only demonstrates generic constraints.

Let's see what we can do in each of the Wash methods. In the first Wash method, we can:

public static void Wash(Stack<Animal> animals) { 
    animals.Push(new Animal());
    Animal a = animals.Pop();
}

Now suppose you have a Stack<Bear> bears;, and you want to pass it into the first Wash. Do you see how this will create a contradiction if the compiler allowed you to do this? You can't actually add an Animal to a Stack<Bear>! But as far as Wash is concerned, adding an Animal is totally fine, because it only knows that it can accept a Stack<Animal>!

Therefore, Stack<Bear> is not a subtype of Stack<Animal>, because you cannot add Animals to the former, but you can to the latter.

In the second Wash method, although you can pass bears to it, you can't add Animals to ti anymore:

public static void Wash<T>(Stack<T> animals) where T: Animal { 
    animals.Push(new Animal()); // error
    Animal a = animals.Pop();
}

Because the compiler isn't sure that Stack<T> is a Stack<Animal>. It might be, but it also could be a Stack<Bear>, or a Stack<Unicorn> or Stack<SomeOtherSubclassOfAnimal>, right?

Related