все еще не понимает ковариации и контравариантности & in / out

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

из здесь

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

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

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

и здесь

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

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

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

45
задан Community 23 May 2017 в 11:47
поделиться

3 ответа

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

Ковариация

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

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

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

Contravariance

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

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

Это означает, что если у нас есть Func , его можно преобразовать в Func .

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

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

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

47
ответ дан 26 November 2019 в 20:56
поделиться

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

Представьте, что у вас есть 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 - только одну » а ". Итак, когда вы сделаете это ...

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 
}

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

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

или

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
}

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

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

Заранее приносим свои извинения за допущенные ошибки =)

56
ответ дан 26 November 2019 в 20:56
поделиться

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

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

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

7
ответ дан 26 November 2019 в 20:56
поделиться
Другие вопросы по тегам:

Похожие вопросы: