RAGRETS.MD

A while back Swift developer Joe Groff tweeted a proposal that projects have a REGRETS.MD file describing "regretful design choices that can't be reversed for time, compatibility, etc. reasons." This seems like a good idea, both to help developers who are trodding similar ground, and also for me to vent some frustration with being unable to rewind time.

To that end, I'm starting a blog series, RAGRETS.MD, where I will list some of the design decisions I regret about C#. For the most part these are my personal regrets, but in cases where I know the feeling is shared widely among the language or development teams I'll do my best to point it out.

To christen the series I might as well start with one of the most universally reviled "features" in the C# language: covariant arrays. Everyone I know of in the language and the development teams who has an opinion on the feature thinks it's bad.

For those unfamiliar, the feature is simple: in C# an array of type T[] can be converted to an array of type U[] if T is a subtype of U. For instance, an array of string[] can be converted to an array of type object[] without copying the array. As far as I know, there are two reasons the feature was put into C#. The first is simple; Java has it. C# was meant to be familiar to Java users and reasonably compatible with its source code. The second reason is probably the reason why Java has it: it's quite useful if your language doesn't have generics.

Consider how you would write a simple helper method to print an array of objects without generics. It would probably look something like:

void PrintAll(object[] objects)
{
    foreach (var o in objects)
    {
        Console.WriteLine(o.ToString());
    }
}

This seems like a very useful method, but what if you want to use it to print a list of strings? If there's a conversion from string[] to object[], no problem. But if there isn't you have to copy the whole list. It's pretty easy to see why covariant arrays are attractive.

So what's the problem? Well, the first problem is that it's completely type-unsafe given that arrays are mutable. Consider the following example:

string[] s = new[] { "a", "b", "c" };
object[] o = s;
o[0] = 5;

This should fail at compile time -- you're putting an integer into a string array! Indeed, if you replace o[0] with s[0] on the last line, you'll get a compile time error saying this is illegal. But covariant arrays violate compile-time type safety. Of course, C# is supposed to be a memory safe language, so it doesn't just do nothing about this. Instead it adds a runtime check to every write of an array, just in case there's a type safety violation, at which point an exception is thrown.

The second problem is that array covariance is just not that useful if you have interfaces and generics that understand variance. Later versions of C# added generics and variance to interfaces like IEnumerable<T>, which is a simple, type safe approach to writing the PrintAll method above. The key is that IEnumerable<T> doesn't allow modification of the elements of the array, so you can never alter the array after the conversion.

So the sum of the situation is: any array write could potentially throw an exception and all array accesses suffer a performance hit, just for a feature that almost no one uses and has clearly better alternatives.

Regrets.