Array covariance is broken? Yes it is!
The code examples I show here applies to C# and it applies to Java as well, because I think Java was the first to make(break?) this. C# unwillingly had to do the same in its language.
Variance (Covariance, contravariance, invariance) is a hard topic to explain. But I am going to try anyway.
When designing a language with types, the designers must set certain rules about how types and its related types are substitutable. Any operation dealing with types is subjected to those rules. An operation could be covariant, contravariant or invariant. Note that an operation could not just be a function/method, but could also be an assignment.
Consider these classes. Small
and Little
are both subclasses of Big
.
public class Big
{
}
public class Small : Big
{
}
public class Little : Big
{
}
If I can assign a Small
type to a Big
type, then that assignment operation was covariant with those types.
Big a = new Small();
Big b = new Little();
The converse is not true for the above operation. Contraviance is the converse of covariance. Covariance feels more intuitive than contravariance. The assigment operation is not contravariant with those types. If you try to do this, the compiler should warn you, because the rules of variance are well defined.
Small s = new Big();
These are very simplified examples. If you would like me to cover this topic elaborately in another post, please let me know in the comments.
In this post I am going to talk only about broken array covariance.
The language designers decided to make arrays covariant. The following code is perfectly compile-able.
Big[] bigArray = new Small[10];
The above assigment operation is legal, because it obeys the rules of variance.
Now consider the following code. We are using the bigArray
defined in the previous code. This code also perfectly legal. Because a Small
object is assignable to a Big
type. It satifies the covariance rules.
bigArray[0] = new Small();
Little
assigned to Big
also follows the covariance rules.
bigArray[1] = new Little();
Here's where things go wrong. The above code is still perfectly valid code. The poor compiler does not know that the runtime disagrees.
If you run the program, it will end up with an exception trying to assign Little
to Big
. This is why the array covariance is broken. The problem is, even though the type is Big
, the underlying storage is of Small
. Small
and Little
are not substitutable with each other.
Could the language designers have avoided this problem? May be. Let's create List
instead of array.
List<Big> bigs = new List<Small>();
The above code does not compile. The problem is completely avoided by the compiler here to keep the types safe.
If you are working with array with related types, before inserting an object into an array, check its type first to avoid runtime errors. Next time when you are reviewing code with arrays, remember array covariance is broken!
Hope you found this interesting. Thank you for your attention.
Cheers 🍺
Top comments (0)