Thursday, October 9, 2014

Generics and variance

When working with generic collections there is often the question: "Why can't I just assign my TList<TPerson> to a variable of TList<TEntity>? TPerson inherits from TEntity!"

There is something called covariance and contravariance - but the bad news are: Delphi does not support variance. But we are trying to understand what this is all about anyway, shall we?

Let's take a look at our TList<TPerson> again and find out why it is invariant as the Wikipedia article says. Invariant means it's not compatible with either TList<TEntity> nor TList<TCustomer> (TCustomer inherits from TPerson). Let's assume we would hardcast it to TList<TEntity> and continue working with it. It seems correct since every TPerson is a TEntity so a TList<TEntity> is a TList<TPerson> as well, no? No! And that is because data goes out and in. Just iterating over the list and taking the items and printing their name would work. But someone could also try to add a TOrder (it inherits from TEntity) into that list. This will work because at that point we have a TList<TEntity> but at a later point this might horribly crash since some other code still uses the list as TList<TPerson> that suddenly contains a TOrder which clearly does not belong there.

Now let's take a look at a TEnumerable<TPerson> that you might hardcast to a TEnumerable<TEntity>. This will work because you can only retrieve things from TEnumerable<T> but not add any. That is also why C# choses the out keyword for marking a generic type as covariant. And that is also why IReadOnlyList<T> is covariant. You can only retrieve items from it but not add any - even if you could remove items it would be covariant but not if you could add some.

Example: Imagine a box of apples. You can treat it as a box of fruits as long as you just take out some or count them. But you are not allowed to add oranges to it.

Now that we understood what covariance is we might understand easier what contravariance is. As its name implies its the opposite of covariance. And indeed it works the other way around. The Wikipedia articles says the subtyping is reversed. Think of an argument of TProc<TCustomer> that is passed somewhere. So for every TCustomer in a list it does something with that instance and what exactly is done is specified in the anonymous method that is getting passed. The code in that method gets a TCustomer and can access anything a TCustomer has. So now if we had contravariance this would apply here and we could actually pass a TProc<TEntity> because we only want access to the properties of a TEntity which is the base type of TCustomer. Now you see that the C# keyword in makes sense for a contravariant generic type.

If we had a labeler at hand it would be covariant because we can label not only apples but all kinds of fruits.

So how to deal with that kind of dilemma in Delphi?
Well there are several approaches to this. One is carefully(!) using hard-casts making sure that nobody does evil things (like adding oranges into the apple box). When using Spring4D collections you can make use of the ElementType property that every interfaced collection type has to make sure you are not adding wrong things to your list. And you can use the IObjectList interface when dealing with lists that contain class types. This is especially useful when connecting lists to GUI elements because they just have to know about IObjectList. It's almost the same as IList<TObject> but with a different guid and QueryInterface only succeeds for a list that contains objects. But most importantly take the correct type for what you are doing. If you just want to iterate things without manipulating then take IEnumerable<T> or IReadOnlyList<T> instead of IList<T> (see encapsulation).

But there is even more about variance and one is very interesting that not even C# does support - but Java and C++ do: return type covariance.

Imagine a TVehicleFactory with a virtual method CreateNew method that returns a TVehicle. Now you inherit a TCarFactory from it which overrides the CreateNew method. It still has TVehicle as result but it really makes more sense to let it return TCar. If that would work we had return type covariance.

I hope this article shed a bit more light on generics and variance and why certain things don't work the way one might expect.