Почему ковариантность массивов типов-значений несогласована?

Еще один интересный вопрос со StackOverflow:

uint[] foo = new uint[10];
object bar = foo;
Console.WriteLine("{0} {1} {2} {3}",       
  foo is uint[], // True
  foo is int[],  // False
  bar is uint[], // True
  bar is int[]); // True

Что за ерунда тут происходит?

Этот фрагмент кода иллюстрирует интересное, но неудачное противоречие между системой типов CLI и системой типов C#.

В CLI есть концепция «совместимости по присваиванию». Если значение x известного типа S является «совместимым по присваиванию» с местом хранения y известного типа T, то вы можете записать x в y. Если нет, то попытка это сделать не является верифицируемым кодом и верификатор это запретит.

Система типов CLI говорит, например, что подтипы ссылочного типа совместимы по присваиванию с супертипами ссылочного типа. Если у вас есть string, то её можно сохранить в переменной типа object, потому что оба типа – ссылочные, и string – подтип object. Но обратное неверно; супертипы не совместимы по присваиванию с подтипами. Вы не сможете засунуть что-то, известное как object, в переменную типа string без предварительного приведения типа.

В сущности, «совместим по присваиванию» означает «имеет смысл засовывать эти конкретные биты в эту переменную». Присваивание исходных данных в переменную назначения должно «сохранять представление».

Одно из правил CLI – в том, что «если X совместим по присваиванию с Y , то X [] совместим по присваиванию с Y [] ».

То есть, массивы ковариантны по отношению к совместимости по присваиванию. Как я уже обсуждал, это на самом деле сломанный вид ковариантности.

Такого правила нет в C#. Правило ковариантности массивов в C# таково: «если X – ссылочный тип, неявно приводимый к ссылочному типу Y , то X [] неявно приводим к Y [] ». Это слегка другое правило!

В CLI, uint и int совместимы по присваиванию; так что uint[] и int[] тоже. Но в C#, преобразования между int и uint явные, а не неявные, и это типы-значения, а не ссылочные типы. Так что в C# запрещено конвертировать int[] в uint[]. Но это разрешено в CLI. Так что теперь мы стоим перед выбором.

1) Реализовать “is” так, чтобы, когда компилятор не смог статически определить результат, то он вставлял бы вызов метода, который проверяет все правила C# по проверке конвертируемости, сохраняющей представление. Это медленно, и в 99.9% случаев совпадает с результатом применения правил CLI. Но мы принимаем потерю производительности для 100% совместимости с правилами C#.

2) Реализовать “is” так, что когда компилятор не смог статически определить результат, то он полагался бы на невероятно быструю проверку совместимости по присваиванию из CLR, и жить с тем фактом, что она говорит, что uint[] это int[], несмотря на то, что это на самом деле не так в C#.

Мы выбрали последнее. Не очень хорошо, что спецификации C# и CLI расходятся в этом мелком вопросе, но мы готовы жить с этим противоречием.

Так что здесь происходит то, что в случаях «foo», компилятр статически может определить, каков будет результат в соответствии с правилами C#, и генерирует код для порождения «True» и «False». Но в случаях «bar», компилятор уже не знает точный тип того, что лежит в bar, так что он генерирует код, чтобы заставить CLR отвечать на вопрос, и CLR высказывает другое мнение.