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.