хорошо, я немного читал эту тему по stackoverflow, смотрел это & это , но все еще немного сбит с толку насчет со / противоречий.
из здесь
Ковариация позволяет «больше» (меньше определенный) тип, который будет заменен в API, где только оригинальный тип используется в «выходной» позиции (например, как возвращаемое значение). Контравариантность позволяет «меньший» (более конкретный) тип заменен в API, где оригинальный тип используется только в позиция «ввода».
Я знаю, что это связано с безопасностью типов.
о входе / выходе
. Могу ли я сказать, что я использую в
, когда мне нужно записать в него, и из
, когда он только для чтения. и в
означает контрастность, вне
ковариантность. но из приведенного выше объяснения ...
и здесь
Например,
List
не может бытьСписок
потому чтоlist.Add (new Apple ())
действителен для List, но не дляList
.
, так не должно быть, если бы я использовал в
/ собираюсь записать объект, он должен быть больше более общий.
Я знаю, что этот вопрос был задан, но все еще очень запутан.
Как ковариация, так и контравариантность в C # 4.0 относятся к возможности использования производного класса вместо базового. Ключевые слова in / out - это подсказки компилятора, указывающие, будут ли параметры типа использоваться для ввода и вывода.
Ковариация в C # 4.0 поддерживается ключевым словом out
, и это означает, что универсальный тип, использующий производный класс параметра типа out
, подходит. Следовательно,
IEnumerable<Fruit> fruit = new List<Apple>();
Поскольку Apple
является Fruit
, List
можно безопасно использовать как IEnumerable
Контравариантность - это ключевое слово в
, которое обозначает типы ввода, обычно в делегатах. Принцип тот же, это означает, что делегат может принимать более производный класс.
public delegate void Func<in T>(T param);
Это означает, что если у нас есть Func
, его можно преобразовать в Func
.
Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;
Потому что, хотя принцип один и тот же, безопасное приведение от производного к базовому типу, когда оно используется для входных типов, мы можем безопасно привести меньшее производного типа ( Func
) до более производного типа ( Func
), что имеет смысл, поскольку любая функция, которая принимает Fruit
, может также возьмите Apple
.
Мне пришлось долго и усердно думать, как это хорошо объяснить. Объяснить это так же сложно, как и понять.
Представьте, что у вас есть 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, когда общий тип будет использоваться только как возвращаемый тип методов интерфейса. Используется, когда он будет использоваться в качестве параметра метода. Те же правила применяются и при использовании делегатов.
Бывают странные исключения, но я не буду здесь о них беспокоиться.
Заранее приносим свои извинения за допущенные ошибки =)
Ковариацию довольно легко понять. Это естественно. Контравариантность более сбивает с толку.
Внимательно посмотрите на этот пример из MSDN . Посмотрите, как SortedList ожидает IComparer, но они передают ShapeAreaComparer: IComparer. Shape - это «больший» тип (он находится в сигнатуре вызываемого, а не вызывающего), но контравариантность позволяет «меньшему» типу - Circle - заменять везде в ShapeAreaComparer, который обычно принимает Shape.
Надеюсь, что это поможет.