все еще не понимают ковариации и контравариантности и в / из - PullRequest
43 голосов
/ 10 августа 2010

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

из здесь

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

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

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

и здесь

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

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

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

Ответы [ 6 ]

55 голосов
/ 10 августа 2010

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

Представьте, что у вас есть базовый класс 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.

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

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;

Вы бы получилиt ошибка типа «не может неявно преобразовать из ...»

У вас есть два варианта: 1) явное преобразование их или 2) указание компилятору неявно преобразовывать их.

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
}

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

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

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

43 голосов
/ 10 августа 2010

И ковариация, и контравариантность в 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.

7 голосов
/ 03 ноября 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(), ожидает млекопитающего, IInvariant<Dog>.Get() - возвращает пса каждая собака является млекопитающим, поэтому она совместима .
  • Тот, кто звонит 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 универсальный модификатор) - это скучно
  • Ни один из моих методов не принимает Млекопитающее -это интересно, хотя это фактическое ограничение 1121 *, наложенное геном outric модификатор

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

При использовании * имеется контрастность1188 * универсальный модификатор (см. Выше)

Если наш тип выглядит так: 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. "Ковариант" короче "контраваратинга", и это напротив длины их модификаторов ("out" и "in" соответственно)
  2. против varaint немного счетчик интуитивно понятно (см. Пример выше)
7 голосов
/ 10 августа 2010

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

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

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

5 голосов
/ 06 октября 2017

В словах Джонса:

Ковариация позволяет заменять «больший» (менее конкретный) тип *1005* в API, где исходный тип используется только в позиции «вывода» (например, в качестве возвращаемого значения). Контравариантность допускает замену «1007» «меньшего» (более конкретного) типа в 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();
3 голосов
/ 14 января 2017

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

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

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

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

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

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, 

        }
    }
}
...