Почему дисперсия параметра типа класса должна соответствовать дисперсии параметров типа возвращаемого/аргумента своего метода?

Ниже приводятся жалобы:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

но я не могу представить, где это не было бы безопасным для типа. (snip *) Является ли это причиной того, что это запрещено, или есть ли другой случай, который нарушает безопасность типа, о которой я не знаю?


* Мои первоначальные мысли были, по общему признанию, запутаны, но, несмотря на это, ответы очень тщательны, и @Theodoros Chatzigiannakis даже с большой точностью проанализировал мои первоначальные предположения.

Наряду с хорошим ударом от ретроспективы я понимаю, что ложно предположил, что сигнатура типа ICovariant::M остается Func<IInvariant<Derived>>, когда ее ICovariant<Derived> присваивается a ICovariant<Base>. Тогда, приписывая, что M - Func<IInvariant<Base>> будет выглядеть отлично от ICovariant<Base>, но, конечно, будет незаконным. Почему бы просто не запретить этот последний, явно незаконный литье? (так я думал)

Я чувствую, что эта ложная и тангенциальная гипотеза умаляет вопрос, поскольку Эрик Липперт также указывает, но в исторических целях отрывочная часть:

Наиболее интуитивным объяснением для меня является то, что, беря ICovariant в качестве примера, из коварианта TCov следует, что метод IInvariant<TCov> M() можно отличить от некоторого IInvariant<TSuper> M(), где TSuper super TCov, что нарушает инвариантность TInv в IInvariant. Однако эта импликация не представляется необходимой: инвариантность IInvariant на TInv может быть легко реализована путем отказа от приведения M.

Ответ 1

Я не уверен, что вы ответили на ваш вопрос в любом из ответов.

Почему дисперсия параметра типа класса должна соответствовать дисперсии параметров типа возвращаемого/аргумента его методов?

Это не так, поэтому вопрос основан на ложной предпосылке. Фактические правила здесь:

https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

Рассмотрим теперь:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
   IInvariant<TCov> M(); // Error
}

Является ли это причиной того, что это запрещено, или есть ли другой случай, который нарушает безопасность типов, о которой я не знаю?

Я не буду следовать вашим объяснениям, так что давайте просто скажем, почему это запрещено без ссылки на ваше объяснение. Здесь позвольте мне заменить эти типы на некоторые эквивалентные типы. IInvariant<TInv> может быть любым типом, инвариантным в T, скажем ICage<TCage>:

interface ICage<TAnimal> {
  TAnimal Remove();
  void Insert(TAnimal contents);
}

И, возможно, у нас есть тип Cage<TAnimal>, который реализует ICage<TAnimal>.

И заменим ICovariant<T> на

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

Пусть реализует интерфейс:

class TigerCageFactory : ICageFactory<Tiger> 
{ 
  public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
}

Все идет хорошо. ICageFactory является ковариантным, поэтому это законно:

ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
ICage<Animal> animalCage = animalCageFactory.MakeCage();
animalCage.Insert(new Fish());

И мы просто кладем рыбу в клетку тигра. Каждый шаг был совершенно законным, и мы закончили с нарушением системы типов. Вывод, который мы делаем, состоит в том, что не должно было быть законным делать ковариантность ICageFactory в первую очередь.

Посмотрите на свой контравариантный пример; это в основном то же самое:

interface ICageFiller<in T> {
   void Fill(ICage<T> cage);
}

class AnimalCageFiller : ICageFiller<Animal> {
  public void Fill(ICage<Animal> cage)
  {
    cage.Insert(new Fish());
  }
}

И теперь интерфейс контравариантен, поэтому это законно:

ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
tigerCageFiller.Fill(new Cage<Tiger>());

Мы снова поместили рыбу в клетку тигра. Еще раз мы заключаем, что, должно быть, было незаконным, чтобы сделать тип контравариантным в первую очередь.

Итак, теперь рассмотрим вопрос о том, как мы знаем, что они незаконны. В первом случае имеем

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

И соответствующее правило:

Обратные типы всех методов не-void интерфейса должны быть действительными ковариантно.

Является ли ICage<T> "действительным ковариантно"?

Тип действителен ковариантно, если он: 1) тип указателя или не общий класс... NOPE 2) Тип массива... NOPE 3) Тип типа типового типа... NOPE 4) Тип построенного класса, структуры, перечисления, интерфейса или делегата X<T1, … Tk> ДА!... Если параметр i-го типа был объявлен как инвариантный, то Ti должен быть действительным инвариантно.

TAnimal был инвариантен в ICage<TAnimal>, So T в ICage<T> должен быть действительным инвариантно. Это? Нет. Чтобы быть действительным инвариантно, оно должно быть действительным как ковариантно, так и контравариантно, но оно справедливо только ковариантно.

Поэтому это ошибка.

Выполнение анализа для контравариантного случая остается в виде упражнения.

Ответ 2

Посмотрим на более конкретный пример. Мы сделаем пару реализаций этих интерфейсов:

class InvariantImpl<T> : IInvariant<T>
{
}

class CovariantImpl<T> : ICovariant<T>
{
    public IInvariant<T> M()
    {
        return new InvariantImpl<T>();
    }
}

Теперь предположим, что компилятор не пожаловался на это и попытался использовать его простым способом:

static IInvariant<object> Foo( ICovariant<object> o )
{
    return o.M();
}

Пока все хорошо. o - ICovariant<object>, и этот интерфейс гарантирует, что у нас есть метод, который может вернуть IInvariant<object>. Нам не нужно выполнять какие-либо приведения или преобразования, все в порядке. Теперь позвоните по методу:

var x = Foo( new CovariantImpl<string>() );

Поскольку ICovariant является ковариантным, это действительный вызов метода, мы можем заменить ICovariant<string> везде, где что-то хочет ICovariant<object> из-за этой ковариации.

Но у нас есть проблема. Внутри Foo мы вызываем ICovariant<object>.M() и ожидаем, что он вернет IInvariant<object>, потому что это говорит интерфейс ICovariant. Но это не может быть так, потому что фактическая реализация, которую мы передали, фактически реализует ICovariant<string>, а метод M возвращает IInvariant<string>, что не имеет ничего общего с IInvariant<object> из-за инвариантности этого интерфейса. Они совершенно разные.

Ответ 3

Почему дисперсия параметра типа класса должна соответствовать дисперсии параметров типа возвращаемого/аргумента его методов?

Это не так!

Возвращаемые типы и типы аргументов не должны соответствовать дисперсии закрывающего типа. В вашем примере они должны быть ковариантными для обоих типов. Это звучит контр-интуитивно, но причины станут очевидными в объяснении ниже.


Почему ваше предлагаемое решение недействительно

из коварианта TCov следует, что метод IInvariant<TCov> M() можно отличить от некоторого IInvariant<TSuper> M(), где TSuper super TCov, что нарушает инвариантность TInv в IInvariant. Однако эта импликация не представляется необходимой: инвариантность IInvariant on TInv может быть легко реализована путем отказа от приведения M.

  • Что вы говорите, так это то, что общий тип с параметром типа варианта может быть назначен другому типу того же определения общего типа и другого параметра типа. Эта часть верна.
  • Но вы также говорите, что для того, чтобы обойти проблему нарушения потенциального подтипирования, очевидная сигнатура метода не должна изменяться в процессе. Это не правильно!

Например, ICovariant<string> имеет метод IInvariant<string> M(). "Отказ от приведения M" означает, что когда ICovariant<string> присваивается ICovariant<object>, он по-прежнему сохраняет метод с сигнатурой IInvariant<string> M(). Если бы это было разрешено, то этот вполне допустимый метод имел бы проблему:

void Test(ICovariant<object> arg)
{
    var obj = arg.M();
}

Какой тип должен содержать компилятор для типа переменной obj? Должно ли это быть IInvariant<string>? Почему бы не IInvariant<Window> или IInvariant<UTF8Encoding> или IInvariant<TcpClient>? Все они могут быть действительными, см. Сами:

Test(new CovariantImpl<string>());
Test(new CovariantImpl<Window>());
Test(new CovariantImpl<UTF8Encoding>());
Test(new CovariantImpl<TcpClient>());

Очевидно, что статически известный тип возвращаемого метода (M()) не может зависеть от интерфейса (ICovariant<>), реализуемого типом среды выполнения объекта!

Поэтому, когда общий тип присваивается другому родовому типу с более общими аргументами типа, сигнатуры членов, которые используют соответствующие параметры типа, обязательно должны быть заменены на нечто более общее. Нет никакого способа обойти это, если мы хотим сохранить безопасность типов. Теперь посмотрим, что означает "более общее" в каждом случае.


Почему ICovariant<TCov> требует, чтобы IInvariant<TInv> был ковариантным

Для аргумента типа string компилятор "видит" этот конкретный тип:

interface ICovariant<string>
{
    IInvariant<string> M();
}

И (как мы видели выше) для аргумента типа object, компилятор "видит" этот конкретный тип:

interface ICovariant<object>
{
    IInvariant<object> M();
}

Предположим, что тип, реализующий прежний интерфейс:

class MyType : ICovariant<string>
{
    public IInvariant<string> M() 
    { /* ... */ }
}

Обратите внимание, что фактическая реализация M() в этом типе связана только с возвратом IInvariant<string>, и она не заботится о дисперсии. Помните об этом!

Теперь, сделав параметр типа ICovariant<TCov> ковариантным, вы утверждаете, что ICovariant<string> следует назначать ICovariant<object> следующим образом:

ICovariant<string> original = new MyType();
ICovariant<object> covariant = original;

... и вы также утверждаете, что теперь можете это сделать:

IInvariant<string> r1 = original.M();
IInvariant<object> r2 = covariant.M();

Помните, что original.M() и covariant.M() являются вызовами одного и того же метода. И реальная реализация метода знает, что она должна вернуть Invariant<string>.

Итак, в какой-то момент во время выполнения последнего вызова мы неявно преобразуем IInvariant<string> (возвращенный фактическим методом) в IInvariant<object> (что является ковариантной сигнатурой promises). Для этого IInvariant<string> должен быть назначен IInvariant<object>.

Чтобы обобщить, для всех IInvariant<S> и IInvariant<T>, где S : T, должны применяться те же отношения. И это точно описание параметра ковариантного типа.


Почему IContravariant<TCon> также требует, чтобы IInvariant<TInv> был ковариантным

Для аргумента типа object компилятор "видит" этот конкретный тип:

interface IContravariant<object>
{
    void M(IInvariant<object> v); 
}

И для аргумента типа string компилятор "видит" этот конкретный тип:

interface IContravariant<string>
{
    void M(IInvariant<string> v); 
}

Предположим, что тип, реализующий прежний интерфейс:

class MyType : IContravariant<object>
{
    public void M(IInvariant<object> v)
    { /* ... */ }
}

Опять же, обратите внимание, что фактическая реализация M() предполагает, что она получит от вас IInvariant<object>, и она не заботится о дисперсии.

Теперь, создав параметр типа IContravariant<TCon>, вы утверждаете, что IContravariant<object> должен быть назначен для IContravariant<string>, как это...

IContravariant<object> original = new MyType();
IContravariant<string> contravariant = original;

... и вы также утверждаете, что теперь можете это сделать:

IInvariant<object> arg = Something();
original.M(arg);
IInvariant<string> arg2 = SomethingElse();
contravariant.M(arg2);

Опять же, original.M(arg) и contravariant.M(arg2) являются вызовами того же метода. Фактическая реализация этого метода предполагает, что мы передадим что-либо, что IInvariant<object>.

Итак, в какой-то момент во время выполнения последнего вызова мы неявно преобразуем IInvariant<string> (что и требует от нас контравариантная подпись) к IInvariant<object> (это то, что ожидает фактический метод). Для этого IInvariant<string> должен быть назначен IInvariant<object>.

Чтобы обобщить, каждый IInvariant<S> должен быть назначен IInvariant<T>, где S : T. Итак, мы снова смотрим на параметр ковариантного типа.


Теперь вам может быть интересно, почему существует несоответствие. Куда делась двойственность ковариации и контравариантности? Он все еще существует, но в менее очевидной форме:

  • Когда вы находитесь сбоку от выходов, дисперсия ссылочного типа идет в том же направлении, что и дисперсия закрывающего типа. Поскольку охватывающий тип может быть ковариантным или инвариантным в этом случае, ссылочный тип также должен быть ковариантным или инвариантным соответственно.
  • Когда вы находитесь сбоку от входов, дисперсия ссылочного типа идет вразрез с направлением дисперсии охватывающего типа. Так как охватывающий тип может быть контравариантным или инвариантным в этом случае, ссылочный тип должен теперь быть ковариантным или инвариантным соответственно.