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

Ковариация и контравариантность в языках программирования

Может кто-нибудь объяснить мне, понятие ковариации и контравариантности в теория языков программирования?

4b9b3361

Ответ 1

Ковариация довольно проста и лучше всего рассматривается с точки зрения некоторого класса коллекции List. Мы можем параметризовать класс List с помощью некоторого параметра типа T. То есть наш список содержит элементы типа T для некоторого T. Список будет ковариантным, если

S является подтипом T, если List [S] является подтипом List [T]

(Где я использую математическое определение iff для обозначения того и только тогда, когда.)

То есть a List[Apple] - List[Fruit]. Если есть какая-то процедура, которая принимает List[Fruit] как параметр, и у меня есть List[Apple], тогда я могу передать это как действительный параметр.

def something(l: List[Fruit]) {
    l.add(new Pear())
}

Если наш класс коллекции List является изменяемым, то ковариация не имеет смысла, потому что мы можем предположить, что наша рутина может добавить еще один плод (который не был яблоком), как указано выше. Следовательно, нам нужно, чтобы неизменные классы классов были ковариантными!

Ответ 5

Для дополнительного удобства здесь приведен упорядоченный список ссылок на все статьи Эрика Липперта:

Ответ 6

Существует различие между ковариацией и контравариантности.
Очень грубо говоря, операция ковариантна, если она сохраняет упорядочение типов и контравариантна, если она меняет этот порядок.

Само упорядочение предназначено для представления более общих типов как более крупных, чем более конкретных типов.
Здесь один пример ситуации, когда С# поддерживает ковариацию. Во-первых, это массив объектов:

object[] objects=new object[3];
objects[0]=new object();
objects[1]="Just a string";
objects[2]=10;

Конечно, можно вставить разные значения в массив, потому что в итоге все они происходят из System.Object в .Net framework. Другими словами, System.Object является очень общим или большим типом. Теперь здесь место, где поддерживается ковариация:
присвоение значения меньшего типа переменной большего размера

string[] strings=new string[] { "one", "two", "three" };
objects=strings;

Объекты переменных, которые имеют тип object[], могут хранить значение, которое фактически имеет тип string[].

Подумайте об этом - в какой-то момент, это то, что вы ожидаете, но опять же это не так. В конце концов, в то время как string происходит от object, string[] НЕ НЕ вытекает из object[]. Языковая поддержка ковариации в этом примере делает назначение возможным в любом случае, что вы найдете во многих случаях. Разница - это функция, которая делает язык более интуитивно понятным.

Вопросы, связанные с этими темами, чрезвычайно сложны. Например, на основе предыдущего кода, вот два сценария, которые приведут к ошибкам.

// Runtime exception here - the array is still of type string[],
// ints can't be inserted
objects[2]=10;

// Compiler error here - covariance support in this scenario only
// covers reference types, and int is a value type
int[] ints=new int[] { 1, 2, 3 };
objects=ints;

Пример для работы контравариантности немного сложнее. Представьте себе эти два класса:

public partial class Person: IPerson {
    public Person() {
    }
}

public partial class Woman: Person {
    public Woman() {
    }
}

Woman получается, очевидно, из Person. Теперь рассмотрим, что у вас есть две функции:

static void WorkWithPerson(Person person) {
}

static void WorkWithWoman(Woman woman) {
}

Одна из функций делает что-то (неважно, что) с Woman, другая более общая и может работать с любым типом, полученным из Person. На стороне Woman вещей теперь у вас также есть:

delegate void AcceptWomanDelegate(Woman person);

static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
    acceptWoman(woman);
}

DoWork - это функция, которая может принимать Woman и ссылку на функцию, которая также принимает Woman, а затем передает экземпляр Woman делегату. Рассмотрим полиморфизм элементов, которые у вас есть. Person больше, чем Woman, а WorkWithPerson больше, чем WorkWithWoman. WorkWithPerson также считается большим, чем AcceptWomanDelegate с целью дисперсии.

Наконец, у вас есть три строки кода:

Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
Создается экземпляр

A Woman. Затем вызывается DoWork, передавая экземпляр Woman, а также ссылку на метод WorkWithWoman. Последнее, очевидно, совместимо с типом делегирования AcceptWomanDelegate - одним параметром типа Woman, без возвращаемого типа. Третья строка немного странная. Метод WorkWithPerson принимает параметр Person, а не Woman, как требуется AcceptWomanDelegate. Тем не менее, WorkWithPerson совместим с типом делегирования. Контравариантность позволяет, поэтому в случае делегатов более крупный тип WorkWithPerson может быть сохранен в переменной меньшего типа AcceptWomanDelegate. Еще раз это интуитивная вещь: , если WorkWithPerson может работать с любым Person, передача в Woman не может быть неправильной, правильно?

В настоящее время вам может быть интересно, как все это относится к дженерикам. Ответ заключается в том, что дисперсия может быть применена и к дженерикам. В предыдущем примере использовались массивы object и string. Здесь код использует общие списки вместо массивов:

List<object> objectList=new List<object>();
List<string> stringList=new List<string>();
objectList=stringList;

Если вы попробуете это, вы обнаружите, что это не поддерживаемый сценарий на С#. В С# версии 4.0, а также .NET Framework 4.0, поддержка дисперсии в дженериках была очищена, и теперь можно использовать новые ключевые слова в и вне с общим типа. Они могут определять и ограничивать направление потока данных для определенного параметра типа, что позволяет отклонять работу. Но в случае List<T> данные типа T текут в обоих направлениях - существуют методы типа List<T>, которые возвращают значения T, и другие, которые получают такие значения.

Точка этих направленных ограничений допускает дисперсию, где она имеет смысл, но предотвращает проблемы, такие как ошибка времени выполнения, упомянутая в одном из предыдущих примеров массива. Когда параметры типа правильно оформлены с помощью или вне, компилятор может проверить и разрешить или запретить его отклонение в время компиляции. Microsoft прилагает усилия для добавления этих ключевых слов ко многим стандартным интерфейсам в инфраструктуре .Net, например IEnumerable<T>:

public interface IEnumerable<out T>: IEnumerable {
    // ...
}

Для этого интерфейса поток данных объектов типа T понятен: они могут быть восстановлены только из методов, поддерживаемых этим интерфейсом, а не передаваться им. В результате можно построить пример, аналогичный описанной ранее попытке List<T>, но используя IEnumerable<T>:

IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;

Этот код является приемлемым для компилятора С# с версии 4.0, потому что IEnumerable<T> является ковариантным из-за спецификатора out в параметре типа T.

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

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

Ссылка:

Ответ 7

Bart De Smet имеет отличную запись в блоге о ковариации и контравариантности здесь.

Ответ 8

Оба С# и CLR допускают ковариацию и противоречие вариационных типов, когда привязка метода к делегату. Ковариантность означает, что метод может возвращать тип, который полученный из возвращаемого типа делегатов. Контравариантность означает, что метод может принимать параметр, который является базой для типа параметра делегатов. Например, учитывая делегата определяется следующим образом:

делегировать объект MyCallback (FileStream s);

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

вот так:

Строка SomeMethod (поток s);

Здесь тип возвращаемого типа SomeMethods (String) - это тип, который получен из возвращаемого типа делегатов (Object); эта ковариация разрешена. Тип параметра SomeMethods (Stream) - это тип, который является базовым классом типа параметров делегатов (FileStream); эта противоречивость разрешена.

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

Int32 SomeOtherMethod (поток s);

Несмотря на то, что тип возвращаемого типа SomeOtherMethods (Int32) получен из возврата MyCallbacks type (Object), эта форма ковариации не допускается, поскольку Int32 является типом значения.

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