все еще не понимает ковариации и контравариантности и входа / выхода

хорошо, я прочитал немного по этой теме в stackoverflow, посмотрел это & это, но все еще немного сбит с толку относительно несовпадения / противодействия.

из здесь

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

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

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

и здесь

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

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

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


person Jiew Meng    schedule 10.08.2010    source источник
comment
Я тоже сбит с толку. Прочитав это в Википедии, я почувствовал, что мне нужно провести настоящий анализ 1 и 2, а затем теорию меры, векторные пространства и, наконец, теорию категоризации, прежде чем я получу хорошее интуитивное ощущение. В качестве альтернативы, если кто-то опубликует кучу примеров, я, вероятно, смогу сократить время обучения с 5 семестров до 1 месяца.   -  person Hamish Grubijan    schedule 10.08.2010
comment
Пожалуйста, не сокращайте здесь. Идея состоит в том, чтобы писать по-английски, а не использовать такие вещи, как abt и abit.   -  person John Saunders    schedule 10.08.2010
comment
возможный дубликат C #: Is Variance (Covariance / Contravariance) другое слово для полиморфизма?   -  person nawfal    schedule 07.07.2014


Ответы (6)


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

Ковариация

Ковариации в C # 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;

Почему они называются ко / контравариантностью, если в основном это одно и то же?

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

person Igor Zevaka    schedule 10.08.2010
comment
Упомянутый вами делегат должен быть public delegate void Func<in T>(T param); правильно? ;) - person Jeff Mercado; 10.08.2010
comment
Лучше называть это действием. Func там запутывает :) - person nawfal; 07.07.2014

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

Представьте, что у вас есть 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;
}

и вот класс Banana

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();

помните, что у 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) указать компилятору неявно преобразовать их.

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

or

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;

or

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

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

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

Заранее извиняюсь за неосторожные ошибки =)

person Community    schedule 10.08.2010
comment
Я не в восторге от Java, но разве не должно List<Banana> bananaList = new Banana(); быть List<Banana> bananaList = new List<Banana>();? - person Tyler; 10.08.2010
comment
Фактически, вы можете сказать a = (Apple)f в этом контексте. -1 за некорректный пост, в котором даже не затрагивался вопрос оператора. Да и еще меньше a, b и _4 _... - person Blindy; 10.08.2010
comment
@Tyler Мне не нравится C #, но разве это не C #? - person Wes Field; 31.07.2013
comment
Я думаю, f = (Fruit)b; должно быть f = (Fruit)ba; - person James Wilkins; 25.01.2017
comment
Я думаю, что этот ответ может выиграть от некоторых хороших больших жирных заголовков для разделения разделов. Я следил за этим, пока не добрался до объяснения того, почему вы не можете использовать базовый тип в качестве производного типа, и заблудился, пытаясь пропустить это, потому что я уже знаю, почему лол - person Marie; 21.08.2018

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

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

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

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 и out)

Эксперимент инвариантности

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

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

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

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

А если попробовать: IInvariant<Mammal> invariantMammal = (IInvariant<Dog>)null?

  • Тот, кто звонит IInvariant<Mammal>.Get(), ожидает Mammal, IInvariant<Dog>.Get() - возвращает Dog, каждая собака является млекопитающим, поэтому оно совместимо.
  • Тот, кто звонит 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 вещи:

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

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

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

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

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

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

  • Тот, кто звонит ICovariant<Mammal>.Get(), ожидает Mammal, ICovariant<Dog>.Get() - возвращает Dog, каждая собака является млекопитающим, поэтому оно совместимо.
  • 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 вещи:

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

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

Что, если мы попробуем: 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. «ковариантный» короче, чем «контраваринт», и это противоположно длинам их модификаторов («выход» и «вход» соответственно)
  2. Contra varaint - это небольшой счетчик, интуитивно понятный (см. пример выше)
person Andrzej Gis    schedule 03.11.2018
comment
У меня всегда возникают проблемы с объяснением парадигм кода с помощью абстрактной грамматики, я склоняюсь к этому. Итак, ваше объяснение - это свежий подход, и поэтому первое, что я действительно понял полностью, оно идеально! Спасибо! Вы должны писать книги! - person bradbury; 15.11.2018
comment
Отличное объяснение! - person MultiValidation; 26.08.2020
comment
Один из хороших примеров - как постепенно объяснять сложные концепции ?. @andrzej, ты, должно быть, отличный рассказчик :) - person Parag Meshram; 17.01.2021

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

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

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

person DoctorFoo    schedule 10.08.2010

По словам Йонса:

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

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

// 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();
person woggles    schedule 06.10.2017

Прежде чем перейти к теме, давайте напомним:

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

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

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

Взгляните на простой пример ниже:

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, 

        }
    }
}
person ABajpai    schedule 14.01.2017