Интересно, почему команда С# решила не поддерживать совместную/контравариантность для не-дженериков, учитывая, что они могут быть столь же безопасными. Вопрос довольно субъективный, так как я не ожидаю ответа члена команды, но у кого-то может быть понимание я (и Барбары Лисков).
Давайте посмотрим на этот пример интерфейса:
public interface ITest
{
object Property
{
get;
}
}
Следующая реализация не удастся, хотя и полностью безопасна (мы всегда можем вернуть более конкретный тип без нарушения интерфейса - не в С#, но, по крайней мере, в теории).
public class Test : ITest
{
public string Property
{
get;
}
}
Код, естественно, не был бы безопасным, если бы интерфейс включал сеттер, но это не повод для ограничения реализации в целом, так как это можно было бы указать, используя out/in для объявления безопасности, как и для дженериков.
CLR не поддерживает ковариантные типы возвращаемых данных, тогда как он поддерживает общую дисперсию делегирования/интерфейса с .NET 2.0 и далее.
Другими словами, это действительно не команда С#, а команда CLR.
Что касается того, почему среда CLR не поддерживает нормальную дисперсию - я не уверен, кроме добавления сложности, предположительно, без необходимого количества предполагаемой выгоды.
РЕДАКТИРОВАТЬ: Чтобы противостоять точке ковариации возвращаемого типа, из раздела 8.10.4 спецификации CLI, говорим о "слотах" vtable:
Для каждого нового участника, который отмечен "ожидайте существующий слот", посмотрите, точное совпадение по типу (т.е. поле или метод), имя и подпись и использовать этот слот, если он найден, иначе выделите новый слот.
Из раздела II, раздел 9.9:
В частности, чтобы определить будет ли элемент скрываться (для статических или членов экземпляра) или переопределения (для виртуальные методы) член из базы класса или интерфейса, просто замените каждый общий параметр с его общий аргумент, и сравните итоговые сигнатуры участников.
Нет никаких указаний на то, что сравнение выполняется таким образом, чтобы это допускало отклонение.
Если вы считаете, что CLR допускает отклонение, я думаю, что, учитывая доказательства, приведенные выше, вы можете доказать это с помощью некоторого подходящего IL.
EDIT: Я только что попробовал это в IL, и это не сработает. Скомпилируйте этот код:
using System;
public class Base
{
public virtual object Foo()
{
Console.WriteLine("Base.Foo");
return null;
}
}
public class Derived : Base
{
public override object Foo()
{
Console.WriteLine("Derived.Foo");
return null;
}
}
class Test
{
static void Main()
{
Base b = new Derived();
b.Foo();
}
}
Запустите его, с выходом:
Derived.Foo
Разберите его:
ildasm Test.exe /out:Test.il
Измените Derived.Foo
, чтобы иметь тип возврата "string" вместо "object":
.method public hidebysig virtual instance string Foo() cil managed
Rebuild:
ilasm /OUTPUT:Test.exe Test.il
Перезапустите его, с выходом:
Base.Foo
Другими словами, Derived.Foo больше не переопределяет Base.Foo в отношении CLR.
CLR не поддерживает дисперсию над переопределениями метода, но для реализации интерфейса существует обходное решение:
public class Test : ITest
{
public string Property
{
get;
}
object ITest.Property
{
get
{
return Property;
}
}
}
Это приведет к такому же эффекту, что и ковариантное переопределение, но может использоваться только для интерфейсов и для прямых реализаций
Рассмотрим следующий образец с разрешенным разрешением динамического метода только с разными типами возвращаемых значений (что абсолютно неверно)
public class Test{
public int DoMethod(){ return 2; }
public string DoMethod() { return "Name"; }
}
Test t;
int n = t.DoMethod(); // 1st method
string txt = t.DoMethod(); // 2nd method
object x = t.DoMethod(); // DOOMED ... which one??