Подтвердить что ты не робот

Все еще запутались в отношении ковариации и контравариантности и входа/выхода

Хорошо, я немного читал эту тему на stackoverflow, смотрел это и это, но все еще немного запутался по поводу co/contra-дисперсии.

отсюда

Ковариация позволяет заменять "больший" (менее конкретный) тип в API, где исходный тип используется только в позиции "вывода" (например, в качестве возвращаемого значения). Контравариантность позволяет заменить "меньший" (более конкретный) тип в API, где исходный тип используется только в позиции "ввода".

я знаю, что это связано с безопасностью типов.

о входе in/out. Могу ли я сказать, я использую in когда мне нужно написать к ней, и out когда его только для чтения. и in средствах противоречие, out совпадения. но из объяснения выше...

и здесь

Например, List<Banana> нельзя рассматривать как List<Fruit> поскольку list.Add(new Apple()) действителен для List, но не для List<Banana>.

Так не должно быть, если я собираюсь использовать in/собираюсь записать объект, он должен быть больше, более общим.

Я знаю, что этот вопрос был задан, но все еще очень запутан.

4b9b3361

Ответ 1

Как ковариация, так и контравариантность в С# 4.0 относятся к способности использовать производный класс вместо базового класса. Ключами ввода/вывода являются подсказки компилятора, чтобы указать, будут ли использоваться параметры типа для ввода и вывода.

ковариация

Ковариация в С# 4.0 поддерживается ключевым словом out, и это означает, что общий тип с использованием производного класса параметра типа out в порядке. Следовательно

IEnumerable<Fruit> fruit = new List<Apple>();

Так как Apple является Fruit, List<Apple> можно безопасно использовать как IEnumerable<Fruit>

контрвариация

Контравариантность - это ключевое слово in, и оно обозначает типы ввода, обычно в делегатах. Принцип один и тот же, это означает, что делегат может принимать более производный класс.

public delegate void Func<in T>(T param);

Это означает, что если мы имеем Func<Fruit>, его можно преобразовать в Func<Apple>.

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

Почему они называются co/contravariance, если они в основном одно и то же?

Потому что, несмотря на то, что принцип один и тот же, безопасное литье из производной на базу, при использовании на типах ввода, мы можем смело отнести менее производный тип (Func<Fruit>) к более производному типу (Func<Apple>), что имеет смысл, так как любая функция, которая принимает Fruit, также может принимать Apple.

Ответ 2

мне пришлось долго и упорно думать о том, как объяснить это хорошо. Объяснение, похоже, так же сложно, как и понимание.

Представьте, что у вас есть базовый класс Fruit. И у вас есть два подкласса Apple и Banana.

     Fruit
      / \
Banana   Apple

Вы создаете два объекта:

Apple a = new Apple();
Banana b = new Banana();

Для обоих этих объектов вы можете привести их в объект Fruit.

Fruit f = (Fruit)a;
Fruit g = (Fruit)b;

Вы можете обрабатывать производные классы, как если бы они были их базовым классом.

Однако вы не можете обращаться с базовым классом, как с производным классом

a = (Apple)f; //This is incorrect

Давайте применим это к примеру списка.

Предположим, вы создали два списка:

List<Fruit> fruitList = new List<Fruit>();
List<Banana> bananaList = new List<Banana>();

Вы можете сделать что-то вроде этого...

fruitList.Add(new Apple());

и

fruitList.Add(new Banana());

потому что он по существу приписывает их, когда вы добавляете их в список. Вы можете думать об этом так:

fruitList.Add((Fruit)new Apple());
fruitList.Add((Fruit)new Banana());

Однако использование одной и той же логики в обратном случае приводит к появлению красных флагов.

bananaList.Add(new Fruit());

совпадает с

bannanaList.Add((Banana)new Fruit());

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

На всякий случай ваш вопрос в том, почему это вызывает ошибки, я тоже это объясню.

Здесь класс Fruit

public class Fruit
{
    public Fruit()
    {
        a = 0;
    }
    public int A { get { return a; } set { a = value } }
    private int a;
}

и здесь класс Бананы

public class Banana: Fruit
{
   public Banana(): Fruit() // This calls the Fruit constructor
   {
       // By calling ^^^ Fruit() the inherited variable a is also = 0; 
       b = 0;
   }
   public int B { get { return b; } set { b = value; } }
   private int b;
}

Итак, представьте, что вы снова создали два объекта

Fruit f = new Fruit();
Banana ba = new Banana();

помните, что у бананы есть две переменные "a" и "b", а у Fruit только один, "a". Поэтому, когда вы это делаете...

f = (Fruit)b;
f.A = 5;

Вы создаете полный объект Fruit. Но если бы вы это сделали...

ba = (Banana)f;
ba.A = 5;
ba.B = 3; //Error!!!: Was "b" ever initialized? Does it exist?

Проблема заключается в том, что вы не создаете полный класс Banana. Не все члены данных объявлены/инициализированы.

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

В ретроспективе я должен был отказаться от метафоры, когда попадал в сложный материал

позволяет создавать два новых класса:

public class Base
public class Derived : Base

Они могут делать все, что вам нравится

Теперь давайте определим две функции

public Base DoSomething(int variable)
{
    return (Base)DoSomethingElse(variable);
}  
public Derived DoSomethingElse(int variable)
{
    // Do stuff 
}

Это похоже на то, как работает "выход", вы всегда должны иметь возможность использовать производный класс, как если бы он был базовым классом, примените его к интерфейсу

interface MyInterface<T>
{
    T MyFunction(int variable);
}

Ключевое различие между out/in заключается в том, что Generic используется как возвращаемый тип или параметр метода, это первый случай.

позволяет определить класс, реализующий этот интерфейс:

public class Thing<T>: MyInterface<T> { }

то мы создаем два объекта:

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

Если вы делаете это:

base = derived;

Вы получили бы ошибку, например: "невозможно неявно конвертировать из..."

У вас есть два варианта: 1) явно конвертировать их или 2) сказать, что complier неявно их конвертирует.

base = (MyInterface<Base>)derived; // #1

или

interface MyInterface<out T>  // #2
{
    T MyFunction(int variable);
}

Второй случай входит в игру, если ваш интерфейс выглядит следующим образом:

interface MyInterface<T>
{
    int MyFunction(T variable); // T is now a parameter
}

снова привязывает его к двум функциям

public int DoSomething(Base variable)
{
    // Do stuff
}  
public int DoSomethingElse(Derived variable)
{
    return DoSomething((Base)variable);
}

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

Повторное использование тех же классов

public class Base
public class Derived : Base
public class Thing<T>: MyInterface<T> { }

и те же объекты

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

если вы попытаетесь установить их равными

base = derived;

ваш собеседник снова будет кричать на вас, у вас есть те же параметры, что и раньше

base = (MyInterface<Base>)derived;

или

interface MyInterface<in T> //changed
{
    int MyFunction(T variable); // T is still a parameter
}

В основном использовать, когда generic будет использоваться только как возвращаемый тип методов интерфейса. Используйте это, когда он будет использоваться в качестве параметра Method. Те же правила применяются и при использовании делегатов.

Есть странные исключения, но я не буду беспокоиться о них здесь.

Извините за любые неосторожные ошибки заранее =)

Ответ 3

Ковариация довольно проста для понимания. Это естественно. Контравариантность более запутанная.

Внимательно посмотрите на этот пример из MSDN. Посмотрите, как SortedList ожидает IComparer, но они передаются в ShapeAreaComparer: IComparer. Shape - это "больший" тип (он в сигнатуре вызываемого, а не вызывающего), но контрастность позволяет заменять "меньший" тип - Circle - везде в ShapeAreaComparer, который обычно принимает Shape.

Надеюсь, это поможет.

Ответ 4

Позвольте мне поделиться своим мнением по этой теме.

Отказ от ответственности: игнорируйте нулевые назначения, я использую их, чтобы сделать код относительно коротким, и их достаточно, чтобы увидеть, что компилятор хочет сказать нам.

Начнем с иерархии классов:

class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

Теперь определим некоторые интерфейсы, чтобы показать, что in и out родовых модификаторов на самом деле:

interface IInvariant<T>
{
    T Get(); // ok, an invariant type can be both put into and returned
    void Set(T t); // ok, an invariant type can be both put into and returned
}

interface IContravariant<in T>
{
    //T Get(); // compilation error, cannot return a contravariant type
    void Set(T t); // ok, a contravariant type can only be **put into** our class (hence "in")
}

interface ICovariant<out T>
{
    T Get(); // ok, a covariant type can only be **returned** from our class (hence "out")
    //void Set(T t); // compilation error, cannot put a covariant type into our class
}

Итак, зачем использовать интерфейсы с модификаторами in и out, если они ограничивают нас? Давай посмотрим:


неизменность

Давайте начнем с инвариантности (нет модификаторов in, no out)

Инвариантный эксперимент

Рассмотрим IInvariant<Mammal>

  • IInvariant<Mammal>.Get() - возвращает млекопитающее
  • IInvariant<Mammal>.Set(Mammal) - принимает Млекопитающее

Что если мы попробуем: IInvariant<Mammal> invariantMammal = (IInvariant<Animal>)null?

  • Тот, кто вызывает IInvariant<Mammal>.Get() ожидает млекопитающего, но IInvariant<Animal>.Get() - возвращает Animal. Не каждое животное млекопитающее, поэтому оно несовместимо.
  • Тот, кто называет IInvariant<Mammal>.Set(Mammal) ожидает, что млекопитающее может быть передано. Поскольку IInvariant<Animal>.Set(Animal) принимает любое животное (включая млекопитающее), оно совместимо
  • ВЫВОД: такое назначение несовместимо

А что если мы попробуем: IInvariant<Mammal> invariantMammal = (IInvariant<Dog>)null?

  • Тот, кто вызывает IInvariant<Mammal>.Get() ожидает Mammal, IInvariant<Dog>.Get() - возвращает Dog, каждый Dog - Mammal, поэтому он совместим.
  • Тот, кто называет IInvariant<Mammal>.Set(Mammal) ожидает, что млекопитающее может быть передано. Поскольку IInvariant<Dog>.Set(Dog) принимает только собак (и не всех млекопитающих как собак), это несовместимо.
  • ВЫВОД: такое назначение несовместимо

Давай проверим, правы ли мы

IInvariant<Animal> invariantAnimal1 = (IInvariant<Animal>)null; // ok
IInvariant<Animal> invariantAnimal2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Animal> invariantAnimal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Mammal> invariantMammal1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Mammal> invariantMammal2 = (IInvariant<Mammal>)null; // ok
IInvariant<Mammal> invariantMammal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Dog> invariantDog1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Dog> invariantDog2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Dog> invariantDog3 = (IInvariant<Dog>)null; // ok

ЭТО ОДНО ВАЖНО: Стоит отметить, что в зависимости от того, является ли параметр универсального типа выше или ниже в иерархии классов, сами универсальные типы несовместимы по разным причинам.

Итак, давайте выясним, как мы можем это использовать.


Ковариация (out)

Вы ковариация, когда вы используете out общего модификатора (см выше)

Если наш тип выглядит так: ICovariant<Mammal>, он объявляет 2 вещи:

  • Некоторые из моих методов возвращают Млекопитающее (следовательно, out общего модификатора) - это скучно
  • Ни один из моих методов не принимает Mammal - это интересно, потому что это фактическое ограничение, наложенное модификатором out generic

Как мы можем извлечь выгоду из out ограничений модификаторов? Посмотрите на результаты "эксперимента инвариантности" выше. Теперь попробуйте посмотреть, что произойдет, когда проведете такой же эксперимент для ковариации?

Ковариационный эксперимент

Что если мы попробуем: ICovariant<Mammal> covariantMammal = (ICovariant<Animal>)null?

  • Тот, кто вызывает ICovariant<Mammal>.Get() ожидает Mammal, но ICovariant<Animal>.Get() - возвращает Animal. Не каждое животное млекопитающее, поэтому оно несовместимо.
  • ICovariant.Set(Mammal) - это больше не проблема благодаря ограничениям для модификатора out !
  • ЗАКЛЮЧЕНИЕ такое назначение несовместимо

А что если мы попробуем: ICovariant<Mammal> covariantMammal = (ICovariant<Dog>)null?

  • Тот, кто вызывает ICovariant<Mammal>.Get() ожидает Mammal, ICovariant<Dog>.Get() - возвращает Dog, каждый Dog - Mammal, поэтому он совместим.
  • ICovariant.Set(Mammal) - это больше не проблема благодаря ограничениям для модификатора out !
  • ЗАКЛЮЧЕНИЕ такое назначение СОВМЕСТИМО

Позвольте подтвердить это кодом:

ICovariant<Animal> covariantAnimal1 = (ICovariant<Animal>)null; // ok
ICovariant<Animal> covariantAnimal2 = (ICovariant<Mammal>)null; // ok!!!
ICovariant<Animal> covariantAnimal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Mammal> covariantMammal1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Mammal> covariantMammal2 = (ICovariant<Mammal>)null; // ok
ICovariant<Mammal> covariantMammal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Dog> covariantDog1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Dog> covariantDog2 = (ICovariant<Mammal>)null; // compilation error
ICovariant<Dog> covariantDog3 = (ICovariant<Dog>)null; // ok

Контравариантность (in)

Вы контравариация при использовании in родовом модификатора (см выше)

Если наш тип выглядит так: IContravariant<Mammal>, он объявляет 2 вещи:

  • Некоторые из моих методов принимают Млекопитающее (следовательно, in общем модификаторе) - это скучно
  • Ни один из моих методов не возвращает Mammal - это интересно, потому что это фактическое ограничение, наложенное модификатором in generic.

Контравариантный эксперимент

Что если мы попробуем: IContravariant<Mammal> contravariantMammal = (IContravariant<Animal>)null?

  • IContravariant<Mammal>.Get() - это больше не проблема благодаря ограничениям in модификаторе!
  • Тот, кто называет IContravariant<Mammal>.Set(Mammal) ожидает, что млекопитающее может быть передано. Поскольку IContravariant<Animal>.Set(Animal) принимает любое животное (включая млекопитающее), оно совместимо
  • ВЫВОД: такое назначение СОВМЕСТИМО

А что если мы попробуем: IContravariant<Mammal> contravariantMammal = (IContravariant<Dog>)null?

  • IContravariant<Mammal>.Get() - это больше не проблема благодаря ограничениям in модификаторе!
  • Тот, кто называет IContravariant<Mammal>.Set(Mammal) ожидает, что млекопитающее может быть передано. Так как IContravariant<Dog>.Set(Dog) принимает только собак (и не каждого млекопитающего как собаку), он несовместим.
  • ВЫВОД: такое назначение несовместимо

Позвольте подтвердить это кодом:

IContravariant<Animal> contravariantAnimal1 = (IContravariant<Animal>)null; // ok
IContravariant<Animal> contravariantAnimal2 = (IContravariant<Mammal>)null; // compilation error
IContravariant<Animal> contravariantAnimal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Mammal> contravariantMammal1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Mammal> contravariantMammal2 = (IContravariant<Mammal>)null; // ok
IContravariant<Mammal> contravariantMammal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Dog> contravariantDog1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Dog> contravariantDog2 = (IContravariant<Mammal>)null; // ok!!!
IContravariant<Dog> contravariantDog3 = (IContravariant<Dog>)null; // ok

Кстати, это кажется немного нелогичным, не так ли?

// obvious
Animal animal = (Dog)null; // ok
Dog dog = (Animal)null; // compilation error, not every Animal is a Dog

// but this looks like the other way around
IContravariant<Animal> contravariantAnimal = (IContravariant<Dog>) null; // compilation error
IContravariant<Dog> contravariantDog = (IContravariant<Animal>) null; // ok

Почему не оба?

Таким образом, мы можем использовать как in и out общих модификаторов? - очевидно нет.

Зачем? Вспомните, какие ограничения накладывают модификаторы in и out. Если бы мы хотели сделать наш параметр общего типа ковариантным и контравариантным, мы бы в основном сказали:

  • Ни один из методов нашего интерфейса не возвращает T
  • Ни один из методов нашего интерфейса не принимает T

Что по существу сделало бы наш универсальный интерфейс не универсальным.

Как это запомнить?

Вы можете использовать мои трюки :)

  1. "Ковариант" короче "Контравараинта", и это противоположно длинам их модификаторов ("Out" и "In" соответственно)
  2. contra varaint немного противоречит интуиции (см. пример выше)

Ответ 5

В словах Jons:

Ковариация позволяет использовать "более крупный" (менее конкретный) тип в API, где исходный тип используется только в "выходной" позиции (например, в качестве возвращаемого значения). Контравариантность позволяет заменить "меньший" (более конкретный) тип в API, где исходный тип используется только в позиции "ввода".

Сначала мне показалось, что его объяснение сбивает с толку, но мне было некогда, когда он был замещен, в сочетании с примером из руководства по программированию на С#:

// Covariance.   
IEnumerable<string> strings = new List<string>();  
// An object that is instantiated with a more derived type argument   
// is assigned to an object instantiated with a less derived type argument.   

// Assignment compatibility is preserved.   
IEnumerable<object> objects = strings;

// Contravariance.             
// Assume that the following method is in the class:   
// static void SetObject(object o) { }   
Action<object> actObject = SetObject;  
// An object that is instantiated with a less derived type argument   
// is assigned to an object instantiated with a more derived type argument.   

// Assignment compatibility is reversed.   
Action<string> actString = actObject;    

Делегат конвертера помогает мне понять это:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput представляет ковариацию, где метод возвращает более конкретный тип.

TInput представляет контравариантность, где метод передается менее конкретным типом.

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();

Ответ 6

Перед тем, как приступить к теме, давайте быстро обновим:

Ссылка на базовый класс может содержать объект производного класса, но не наоборот.

ковариация: Ковариация позволяет передавать объект производного типа, где ожидается объект базового типа Ковариация может применяться к делегатам, общим, массивам, интерфейсам и т.д.

контрвариация: Контравариантность применяется к параметрам. Он позволяет назначить метод с параметром базового класса для делегата, который ожидает параметр производного класса

Посмотрите на простой пример ниже:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CovarianceContravarianceDemo
{
    //base class
    class A
    {

    }

    //derived class
    class B : A
    {

    }
    class Program
    {
        static A Method1(A a)
        {
            Console.WriteLine("Method1");
            return new A();
        }

        static A Method2(B b)
        {
            Console.WriteLine("Method2");
            return new A();
        }

        static B Method3(B b)
        {
            Console.WriteLine("Method3");
            return new B();
        }

        public delegate A MyDelegate(B b);
        static void Main(string[] args)
        {
            MyDelegate myDel = null;
            myDel = Method2;// normal assignment as per parameter and return type

            //Covariance,  delegate expects a return type of base class
            //but we can still assign Method3 that returns derived type and 
            //Thus, covariance allows you to assign a method to the delegate that has a less derived return type.
            myDel = Method3;
            A a = myDel(new B());//this will return a more derived type object which can be assigned to base class reference

            //Contravariane is applied to parameters. 
            //Contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.
            myDel = Method1;
            myDel(new B()); //Contravariance, 

        }
    }
}