Почему ковариантность массивов типов-значений несогласована?
Еще один интересный вопрос со 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 высказывает другое мнение.