Ковариация и контравариантность одного аргумента типа

В спецификации C # указано, что тип аргумента не может быть одновременно ковариантным и контравариантным.

Это очевидно, когда при создании ковариантного или контравариантного интерфейса вы украшаете параметры типа «out» или «in» соответственно. Не существует варианта, который позволял бы и то, и другое одновременно ("вне дома").

Является ли это ограничение ограничением для конкретного языка или существуют более глубокие, более фундаментальные причины, основанные на теории категорий, которые заставят вас не хотеть, чтобы ваш тип был одновременно ковариантным и контравариантным?

Изменить:

Насколько я понимаю, массивы на самом деле были ковариантными и контравариантными.

public class Pet{}
public class Cat : Pet{}
public class Siamese : Cat{}
Cat[] cats = new Cat[10];
Pet[] pets = new Pet[10];
Siamese[] siameseCats = new Siamese[10];

//Cat array is covariant
pets = cats; 
//Cat array is also contravariant since it accepts conversions from wider types
cats = siameseCats; 

person William Edmondson    schedule 24.12.2010    source источник
comment
Меня смущает ваше последнее заявление; как получилось, что это демонстрация контравариантности? Сиамский тип более узкий, чем кошка, точно так же, как кошка более узкий тип, чем домашнее животное.   -  person Eric Lippert    schedule 25.12.2010
comment
Все кошки - домашние животные, а все сиамские кошки - кошки, так что это только демонстрирует ковариантность.   -  person Gabe    schedule 25.12.2010
comment
Эрик, ты прав, в этом нет никакого смысла.   -  person William Edmondson    schedule 25.12.2010
comment
Бывают ситуации, когда было бы полезно, чтобы массивы были контравариантными в том же смысле, что и ковариантны, но для записи, а не чтения, то есть если процедура ожидает массив, в котором она может хранить Derived, для нее было бы возможно работа с массивом Base. Лучшим подходом к достижению этого, возможно, было бы для массивов реализовать IReadableByIndex ‹T›, который был бы ковариантным, и IWritableByIndex ‹T›, который был бы контравариантным, поэтому процедуры, которым нужно только читать или писать массив, могли бы сделать правильный выбор.   -  person supercat    schedule 16.07.2011
comment
Между прочим, можно написать процедуру для сортировки любого объекта типа массива, не зная его типа, если объект типа массива поддерживает неуниверсальный интерфейс ISortableByIndex с методами CompareAt (int index1, int index2) и SwapAt (int index1 , int index2).   -  person supercat    schedule 16.07.2011


Ответы (7)


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

Прежде всего, прочтите мою статью на тему «достоверность» дисперсии:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/03/exact-rules-for-variance-validity.aspx.

По определению, если тип «ковариантно действителен», то он не может использоваться контравариантным способом. Если он «контравариантно действителен», то его нельзя использовать ковариантным способом. То, что является одновременно ковариантно и контравариантно действительным, нельзя использовать ни ковариантным, ни контравариантным образом. То есть он инвариантен. Итак, существует объединение ковариантного и контравариантного: их объединение инвариантно.

Во-вторых, давайте на мгновение предположим, что вы исполнили свое желание и что существует аннотация типа, которая работает так, как я думаю, вы хотите:

interface IBurger<in and out T> {}

Предположим, у вас есть IBurger<string>. Поскольку он ковариантен, он может быть преобразован в IBurger<object>. Поскольку он контравариантен, он, в свою очередь, может быть преобразован в IBurger<Exception>, хотя «строка» и «Исключение» не имеют ничего общего. По сути, «вход и выход» означает, что IBurger<T1> может быть преобразован в любой тип IBurger<T2> для любых двух ссылочных типов T1 и T2. Чем это полезно? Что бы вы сделали с такой функцией? Предположим, у вас есть IBurger<Exception>, но на самом деле это IBurger<string>. Что вы могли бы с этим сделать, чтобы оба использовали тот факт, что аргумент типа - это исключение, и позволяли этому аргументу типа быть полной ложью, потому что «реальный» аргумент типа является совершенно несвязанным типом?

Чтобы ответить на ваш последующий вопрос: неявные преобразования ссылочных типов с использованием массивов являются ковариантными; они не контравариантны. Можете ли вы объяснить, почему вы ошибочно считаете их контравариантными?

person Eric Lippert    schedule 25.12.2010
comment
Пример IBurger - это именно то, что я искал. - person William Edmondson; 25.12.2010
comment
Насколько вы можете объяснить, почему вы неправильно считаете их контравариантными после того, как указали на это, очевидно, что это не контравариантность. - person William Edmondson; 25.12.2010
comment
Эрик: Я по-прежнему считаю, что инвариантность - это пересечение, а не объединение ковариантности и контравариантности. Но, возможно, я думаю о наборе типов, которые можно использовать в ковариантном или контравариантном контексте, а не о наборе общих типов, которые действуют ковариантно или контравариантно. - person Ben Voigt; 25.12.2010
comment
Я согласен с тем, что тип, который является как ковариантно действительным, так и инвариантно действительным, не будет инвариантным - его нельзя будет использовать в качестве типа в интерфейсе. Я могу вспомнить случаи, когда ковариация или контравариантность могут быть полезны в интерфейсе маркера, но поскольку интерфейсы могут происходить из нескольких интерфейсов, я не могу придумать случая, когда была бы полезна произвольная дисперсия. Если универсальный интерфейс может быть передан подпрограмме, которая не заботится об универсальном типе, он должен быть производным от неуниверсальной версии, и подпрограмма должна использовать это в качестве своего типа аргумента. - person supercat; 24.03.2011
comment
@ Эрик Липперт: Я нашел этот ответ после ответа на этот вопрос. Единственное, что я хочу сказать, это то, что я понимаю и ценю твою шутку In-N-Out Burger. - person jason; 15.07.2011
comment
@Jason: Рад, что вам понравилось, хотя я вам скажу, хорошо, что основная функция моего чувства юмора - развлекать меня. - person Eric Lippert; 15.07.2011
comment
@ Эрик Липперт: Я понимаю, о чем вы. Моя партнерша постоянно говорит мне, что я рад, тебе это смешно после того, как я рассказываю ей анекдот. - person jason; 15.07.2011

Ковариация и контравариантность исключают друг друга. Ваш вопрос похож на вопрос, может ли набор A быть одновременно надмножеством набора B и подмножеством набора B. Чтобы набор A был как подмножеством, так и надмножеством набора B, набор A должен быть равен набору B, поэтому тогда вы просто спросите, равно ли множество A множеству B.

Другими словами, запрос на ковариацию и контравариантность для одного и того же аргумента аналогичен запросу вообще об отсутствии дисперсии (инвариантность), что является значением по умолчанию. Таким образом, для его определения нет необходимости в ключевом слове.

person Gabe    schedule 24.12.2010
comment
Умм, это возможно, для этого требуется, чтобы A было тождественно равно B. Другими словами, инвариантность. - person Ben Voigt; 24.12.2010
comment
Бен: Спасибо, надеюсь, я разъяснил. - person Gabe; 24.12.2010
comment
Я думал, что инвариантность - это когда не применяются ни ковариация, ни контравариантность. - person William Edmondson; 25.12.2010
comment
Кроме того, разве массивы не являются одновременно ковариантными и контравариантными? Если это так, то эти два понятия не исключают друг друга, потому что функция действительно существует в дикой природе. - person William Edmondson; 25.12.2010
comment
@William: Массивы строго ковариантны. Инвариантность - это пересечение ковариации и контравариантности. - person Gabe; 25.12.2010
comment
@Ben: Дисперсия - это свойство операций с типами, а не с объектами. Свойство типа Cat состоит в том, что его можно присвоить переменной типа Pet. Поскольку объект типа Cat[] может быть назначен переменной типа Pet[], операция [] (массив) имеет свойство ковариации. - person Gabe; 25.12.2010
comment
@Ben: Пожалуйста, определяйте ковариант и подтип по мере их использования, потому что ваши определения явно отличаются от тех, что используются остальными из нас. Мое определение ковариантности не имеет ничего общего с безопасностью типов или подтипами, а мое определение подтипа происходит от (например, IEnumerator<Cat> не является подтипом IEnumerator<Pet>, потому что первое не является производным от второго). - person Gabe; 25.12.2010
comment
Но X[] <: Y[] под Лисковым подразумевает, что X тождественно Y, следовательно, отношение массива инвариантно. - person Ben Voigt; 26.12.2010
comment
Бен: Хотя использование Лискова, заменяемого для вашего определения ковариации, вполне допустимо, при обсуждении C # это не имеет особого смысла. Средство проверки типов C # ничего не знает о Лискове, поэтому его собственные правила подстановки определяют дисперсию. Поскольку компилятор C # позволяет использовать значение типа Cat[] везде, где может использоваться значение типа Pet[], массивы в C # ковариантны. Точно так же IEnumerable стал ковариантным только в C # 4, потому что именно тогда аннотация типа была добавлена ​​к интерфейсу. - person Gabe; 26.12.2010
comment
Бен: Не могу поверить, что мы оба читаем на одном языке. В начале вашей ссылки Эрик заявляет, что массивы, в которых тип элемента является ссылочным, ковариантны. Здесь ничего не говорится о том, чтобы притворяться или не быть на самом деле. Он действительно говорит, что этот конкретный вид ковариации нарушен (потому что он небезопасен), но это далеко от того, чтобы быть инвариантным. - person Gabe; 01.01.2011
comment
@Gabe: Думаю, мы повторяем весь набор комментариев к этому сообщению в блоге. Кстати говоря, вы тот же Гейб, что там комментировал? В любом случае, я думаю, мы оба согласны с тем, что отношение массива является C # -ковариантным (что на самом деле не является типобезопасным), но инвариантным по Лискову. Поэтому я предлагаю для будущих читателей перефразировать свой второй комментарий (# 5 в последовательности), чтобы упомянуть, что C # допускает ковариацию массивов, но (принимая во внимание, что вопрос, заданный о теории), массивы на самом деле являются Лисков- инвариантен, поэтому ковариантное использование нарушено / нетипично - и затем мы уничтожаем этот мешающий беспорядок - person Ben Voigt; 01.01.2011
comment
Кстати: инженеры-разработчики C # также в значительной степени обсуждают ковариацию в C # глубина, включая нарушенная ковариация массивов. - person Ben Voigt; 01.01.2011

Ковариация возможна для типов, которые вы никогда не вводите (например, функции-члены могут использовать его как возвращаемый тип или параметр out, но никогда как входной параметр). Контравариантность возможна для типов, которые вы никогда не выводите (например, как входной параметр, но никогда как возвращаемый тип или параметр out).

Если вы сделали параметр типа как ковариантным, так и контравариантным, вы не могли его вводить и не могли выводить - вы вообще не могли его использовать.

person Ben Voigt    schedule 24.12.2010

Аргумент без ключевых слов - это ковариация, а контравариантность - не так ли?

in означает, что аргумент может использоваться только как тип аргумента функции.

out означает, что аргумент может использоваться только как тип возвращаемого значения.

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

person Arsen Mkrtchyan    schedule 24.12.2010
comment
Поскольку пересечение ковариации и контравариантности является инвариантностью, да. Я думаю, что OP хочет объединить ковариацию и контравариантность, а это просто невозможно. - person Ben Voigt; 24.12.2010

Является ли это ограничение ограничением для конкретного языка или существуют более глубокие, более фундаментальные причины, основанные на теории категорий, которые заставят вас не хотеть, чтобы ваш тип был одновременно ковариантным и контравариантным?

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

Ковариация означает S <: T ⇒ G<S> <: G<T>, а контравариантность означает S <: T ⇒ G<T> <: G<S>. Должно быть совершенно очевидно, что это никогда не может быть правдой одновременно.

person Jörg W Mittag    schedule 25.12.2010
comment
Для меня это не очевидно. Почему очевидно, что это никогда не может быть правдой? Разве это не тот случай, когда на множестве могут быть определены отношения, такие как G ‹S› ‹: G ‹T› и G ‹S››: G ‹T›? Они могут быть бесполезными или интересными отношениями, но я не понимаю, почему они невозможны. - person Eric Lippert; 25.12.2010
comment
@ Эрик Липперт: вы, конечно, правы. В этом конкретном случае я принял <: за обычное отношение подтипов. Тип не может быть одновременно супертипом и подтипом другого типа, если они не являются одинаковым типом. (По крайней мере, я не вижу способа, как это будет работать в системе типов C #.) Если предположить, что S и T - разные типы, я не считаю желательным, чтобы G<S> и G<T> были одного типа. - person Jörg W Mittag; 25.12.2010
comment
Что ж, фактическое отношение в системе типов - это совместимость присваивания, а не подтип; есть тонкие различия. В системе типов CLR int и uint не являются подтипами друг друга, но они совместимы по назначению друг с другом. Поскольку массивы ковариантны в системе типов CLR, int [] и uint [] также совместимы по присваиванию друг с другом, даже если они не являются подтипами. В этом конкретном случае, когда G является типом массива, так получилось, что G ‹int› ‹: G ‹uint› и G ‹int›:› G ‹uint›, но G ‹int›! = G ‹uint›. - person Eric Lippert; 25.12.2010

Что можно сделать с «Ковариантом»?

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

Предположим, у вас есть эти класс и интерфейс:

interface ICanOutput<out T> { T getAnInstance(); }

class Outputter<T> : ICanOutput<T>
{
    public T getAnInstance() { return someTInstance; }
}

Теперь предположим, что у вас есть типы TBig, наследующие TSmall. Это означает, что экземпляр TBig всегда является экземпляром TSmall; но экземпляр TSmall не всегда является экземпляром TBig. (Имена были выбраны так, чтобы их было легко представить, TSmall вписывается в TBig)

Когда вы это сделаете (классический co вариант назначения):

//a real instance that outputs TBig
Outputter<TBig> bigOutputter = new Outputter<TBig>();

//just a view of bigOutputter
ICanOutput<TSmall> smallOutputter = bigOutputter;
  • bigOutputter.getAnInstance() вернет TBig
  • And because smallOutputter was assigned with bigOutputter:
    • internally, smallOutputter.getAnInstance() will return TBig
    • И TBig можно преобразовать в TSmall
    • преобразование выполнено, и на выходе будет TSmall.

Если наоборот (как если бы это был вариант против):

//a real instance that outputs TSmall
Outputter<TSmall> smallOutputter = new Outputter<TSmall>();

//just a view of smallOutputter
ICanOutput<TBig> bigOutputter = smallOutputter;
  • smallOutputter.getAnInstance() вернет TSmall
  • And because bigOutputter was assigned with smallOutputter:
    • internally, bigOutputter.getAnInstance() will return TSmall
    • Но TSmall нельзя преобразовать в TBig !!
    • Тогда это невозможно.

Вот почему типы «против вариантов» не могут использоваться в качестве выходных типов.


Что можно сделать с "Контравариантом"?

Следуя той же идее, описанной выше, контравариант использует модификатор in, что означает, что тип может быть входным параметром метода, но не выходным параметром.

Предположим, у вас есть эти класс и интерфейс:

interface ICanInput<in T> { bool isInstanceCool(T instance); }

class Analyser<T> : ICanInput<T>
{
    bool isInstanceCool(T instance) { return instance.amICool(); }
}

Снова предположим, что типы TBig наследуют TSmall. Это означает, что TBig может делать все, что делает TSmall (у него есть все TSmall участников и больше). Но TSmall не может делать все, что делает TBigTBig больше участников).

Когда вы это сделаете (классический вариант против):

//a real instance that can use TSmall methods
Analyser<TSmall> smallAnalyser = new Analyser<TSmall>();
    //this means that TSmall implements amICool

//just a view of smallAnalyser
ICanInput<TBig> bigAnalyser = smallAnalyser;
  • smallAnalyser.isInstanceCool:
    • smallAnalyser.isInstanceCool(smallInstance) can use the methods in smallInstance
    • smallAnalyser.isInstanceCool(bigInstance) также может использовать методы (он смотрит только на TSmall часть TBig)
  • And since bigAnalyser was assigned with smallAnalyer:
    • it's totally ok to call bigAnalyser.isInstanceCool(bigInstance)

Если было наоборот (как если бы это был co вариант):

//a real instance that can use TBig methods
Analyser<TBig> bigAnalyser = new Analyser<TBig>();
    //this means that TBig has amICool, but not necessarily that TSmall has it    

//just a view of bigAnalyser
ICanInput<TSmall> smallAnalyser = bigAnalyser;
  • For bigAnalyser.isInstanceCool:
    • bigAnalyser.isInstanceCool(bigInstance) can use the methods in bigInstance
    • но bigAnalyser.isInstanceCool(smallInstance) не может найти TBig методов в _56 _ !!! И не гарантируется, что этот smallInstance даже TBig преобразованный.
  • And since smallAnalyser was assigned with bigAnalyser:
    • calling smallAnalyser.isInstanceCool(smallInstance) will try to find TBig methods in the instance
    • и он может не найти TBig методы, потому что этот smallInstance не может быть экземпляром TBig.

Вот почему типы «co variant» не могут использоваться в качестве входных параметров.


Присоединение к обоим

Что же произойдет, если сложить две «канноты»?

  • Не может это + не может это = ничего не может

Что вы могли сделать?

Я не тестировал это (пока ... я думаю, будет ли у меня причина для этого), но, похоже, все в порядке, если вы знаете, что у вас будут некоторые ограничения.

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

  • Один интерфейс, использующий in и имеющий только методы, которые не выводят T
  • Другой интерфейс, использующий out только методы, которые не принимают T в качестве входных данных

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

person Daniel Möller    schedule 19.04.2018

Параметры универсального типа не могут быть одновременно ковариантными и контравариантными.

Почему? Это связано с ограничениями, которые накладывают модификаторы in и out. Если бы мы хотели сделать параметр общего типа ковариантным и контравариантным, мы бы сказали:

  • Ни один из методов нашего интерфейса не возвращает T
  • Ни один из методов нашего интерфейса не принимает T

Что, по сути, сделало бы наш общий интерфейс не универсальным.

Я подробно объяснил это в другом вопросе:

person Andrzej Gis    schedule 03.11.2018