Разница между ковариацией и противоречием
Мне трудно понять разницу между ковариацией и контравариантностью.
- Почему ковариация и контравариантность не поддерживают тип ценности
- Почему C # (4.0) не допускает совпадения и контравариантность в типах универсального classа?
- Generics: List совпадает с List ?
- ref и out в C # и не может быть помечен как вариант
- Почему не делегировать работу по контравариантности со значениями?
Вопрос: «В чем разница между ковариацией и контравариантностью?»
Ковариация и контравариантность – это свойства функции отображения, которая связывает один элемент множества с другим . Более конкретно, отображение может быть ковариантным или контравариантным относительно отношения на этом множестве.
Рассмотрим следующие два подмножества множества всех типов C #. Первый:
{ Animal, Tiger, Fruit, Banana }.
Во-вторых, это явно связанное множество:
{ IEnumerable, IEnumerable, IEnumerable, IEnumerable }
Существует операция отображения от первого набора ко второму набору. То есть для каждого T в первом наборе соответствующий тип во втором наборе IEnumerable
. Или, в краткой форме, отображение T → IE
. Обратите внимание, что это «тонкая стрелка».
Со мной до сих пор?
Теперь давайте рассмотрим отношение . В первом наборе есть соотношение совместимости присваивания между парами типов. Значение типа Tiger
может быть присвоено переменной типа Animal
, поэтому эти типы называются «совместимыми с назначением». Давайте напишем: «значение типа X
может быть присвоено переменной типа Y
» в более короткой форме: X ⇒ Y
Обратите внимание, что это «жирная стрела».
Итак, в нашем первом подмножестве, вот все отношения совместимости присваивания:
Tiger ⇒ Tiger Tiger ⇒ Animal Animal ⇒ Animal Banana ⇒ Banana Banana ⇒ Fruit Fruit ⇒ Fruit
В C # 4, который поддерживает совместимость ковариантного присваивания определенных интерфейсов, существует взаимосвязь совместимости присваивания между парами типов во втором наборе:
IE ⇒ IE IE ⇒ IE IE ⇒ IE IE ⇒ IE IE ⇒ IE IE ⇒ IE
Заметим, что отображение T → IE
сохраняет существование и направление совместимости присваивания . То есть, если X ⇒ Y
, то также верно, что IE
.
Если у нас есть две вещи по обе стороны от толстой стрелки, то мы можем заменить обе стороны на что-то справа от соответствующей тонкой стрелки.
Отображение, обладающее этим свойством по отношению к определенному соотношению, называется «ковариантным отображением». Это должно иметь смысл: последовательность тигров может быть использована там, где необходима последовательность животных, но противоположность неверна. Последовательность животных не обязательно может быть использована, когда необходима последовательность тигров.
Это ковариация. Теперь рассмотрим это подмножество множества всех типов:
{ IComparable, IComparable, IComparable, IComparable }
теперь мы имеем отображение от первого множества к третьему множеству T → IC
.
В C # 4:
IC ⇒ IC IC ⇒ IC Backwards! IC ⇒ IC IC ⇒ IC IC ⇒ IC Backwards! IC ⇒ IC
То есть отображение T → IC
сохранило существование, но изменило направление совместимости присваивания. То есть, если X ⇒ Y
, то IC
.
Отображение, сохраняющее, но меняющее обратное отношение, называется контравариантным отображением.
Опять же, это должно быть четко правильным. Устройство, которое может сравнивать двух животных, также может сравнивать двух тигров, но устройство, которое может сравнивать двух тигров, не обязательно может сравнивать любые два Животные.
Вот в чем разница между ковариацией и контравариантностью в C # 4. Ковариация сохраняет направление назначения. Контравариантность меняет его.
Наверное, проще всего привести примеры – это, конечно же, как я их помню.
ковариации
Канонические примеры: IEnumerable
, Func
Вы можете конвертировать из IEnumerable
в IEnumerable
, или Func
в Func
. Значения выходят только из этих объектов.
Это работает, потому что, если вы только извлекаете значения из API и вернете что-то конкретное (например, string
), вы можете обработать это возвращаемое значение как более общий тип (например, object
).
контрвариация
Канонические примеры: IComparer
, Action
Вы можете конвертировать из IComparer
в IComparer
или Action
в Action
; значения только входят в эти объекты.
На этот раз это работает, потому что если API ожидает что-то общее (например, object
), вы можете дать ему что-то более конкретное (например, string
).
В более общем смысле
Если у вас есть интерфейс IFoo
он может быть ковариантным в T
(т. IFoo
Объявить его как IFoo
если T
используется только в выходной позиции (например, тип возврата) в интерфейсе. Он может быть контравариантным в T
(т.е. IFoo
), если T
используется только во входной позиции (например, тип параметра).
Это становится потенциально запутанным, потому что «выходное положение» не так просто, как кажется – параметр типа Action
по-прежнему использует только T
в выходной позиции – контравариантность Action
поворачивает его, если вы посмотрим, что я имею в виду. Это «выход» в том смысле, что значения могут перейти от реализации метода к коду вызывающего абонента, точно так же, как и возвращаемое значение. К счастью, такого рода вещи не приходят 🙂
Надеюсь, мой пост поможет получить языковой взгляд на тему.
Для наших внутренних тренировок я работал с замечательной книгой «Smalltalk, Objects and Design (Chamond Liu)», и я перефразировал следующие примеры.
Что означает «последовательность»? Идея состоит в том, чтобы проектировать иерархии типа типа с сильно замещаемыми типами. Ключом к получению этой согласованности является соответствие типа субтипа, если вы работаете на статически типизированном языке. (Здесь мы обсудим Принцип замещения Лискова (LSP)).
Практические примеры (псевдокод / недопустимый в C #):
-
Ковариация. Предположим, что птицы, которые постоянно «ставят» яйца, «ставят»: если тип Bird закладывает яйцо, не будет ли подтип птиц подтипом яйца? Например, тип Duck содержит DuckEgg, тогда задается согласованность. Почему это непротиворечиво? Потому что в таком выражении:
Egg anEgg = aBird.Lay();
ссылка aBird может быть юридически заменена птицей или экземпляром утки. Мы говорим, что тип возврата ковариант к типу, в котором определяется Lay (). Переопределение подтипа может возвращать более специализированный тип. => «Они доставляют больше». -
Контравариантность: предположим, что пианисты могут играть «последовательно» со статической типизацией: если пианист играет на фортепиано, сможет ли она сыграть Гран-Пиано? Не будет ли виртуоз играть Гран-Пиано? (Будьте осторожны, есть поворот!) Это непоследовательно! Потому что в таком выражении:
aPiano.Play(aPianist);
aPiano не может быть законно заменен фортепиано или экземпляром GrandPiano! «GrandPiano» может играть только виртуоз, пианисты слишком общие! GrandPianos должен воспроизводиться более общими типами, тогда игра последовательна. Мы говорим, что тип параметра контравариантен типу, в котором определено Play (). Переопределение подтипа может принимать более обобщенный тип. => «Они требуют меньше».
Вернуться к C #:
Поскольку C # – это, в основном, статически типизированный язык, «местоположения» интерфейса типа, которые должны быть согласованными или контравариантными (например, параметры и типы возвращаемых данных), должны быть помечены явно, чтобы гарантировать последовательное использование / разработку этого типа, чтобы сделать LSP работает нормально. В динамически типизированных языках согласованность LSP обычно не является проблемой, другими словами, вы можете полностью избавиться от совместной и контравариантной «разметки» на .Net-интерфейсах и делегатах, если вы использовали только динамический тип в своих типах. – Но это не лучшее решение в C # (вы не должны использовать динамический интерфейс в общедоступных интерфейсах).
Назад к теории:
Описанное соответствие (ковариантные типы возврата / контравариантные типы параметров) является теоретическим идеалом (поддерживается языками Emerald и POOL-1). Некоторые языки OOP (например, Eiffel) решили применить другой тип согласованности, особенно. также ковариантные типы параметров, поскольку он лучше описывает реальность, чем теоретический идеал. В статически типизированных языках желаемая консистенция часто достигается путем применения шаблонов проектирования, таких как «двойная диспетчеризация» и «посетитель». Другие языки предоставляют так называемую «множественную отправку» или несколько методов (это в основном выбор перегрузки функций во время выполнения , например, с помощью CLOS) или получения желаемого эффекта с помощью динамического набора.
Если вы хотите назначить какой-либо метод делегату, подпись метода должна точно соответствовать подписи делегата. Сказав это, ковариация и контравариантность позволяют обеспечить некоторую степень гибкости при сопоставлении подписи методов с методами делегатов.
Вы можете обратиться к этой статье, чтобы понять ковариацию, контравариантность и различия между ними .
Делегат конвертера помогает мне понять разницу.
delegate TOutput Converter(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 dogs = new List () { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } }; List poodles = dogs.ConvertAll(new Converter(ConvertDogToPoodle)); poodles[0].DoBackflip();
Лучше, если я объясню, почему ковариация возвращаемого типа и входная параметрическая контравариантность в C # недопустимы.
Хирург, BabySitter и Plumber происходят от classа Person
public class Surgeon : Person { } public class Plumber : Person { } public class BabySitter : Person { } public class Person { } // We want to restrict performing surgeries only to doctors. public void PerformSurgery(Surgeon doctor) { // If we pass in a Person to PerformSurgery he/she might be a plumber // and we don't want plumbers performing surgeries doctor.Operate(); } // This also makes sense for fixing pipeleaks // If we pass in a Person to FixPipeLeak he/she might be a babysitter public void FixPipeLeak(Plumber plumber) { plumber.FixPlumbing(); }
PerformSurgery(new Person())
и FixPipeLeak(new Person())
генерируют ошибку компиляции. Поэтому мы не хотим, чтобы контравариантность в наших входных параметрах
Что мы хотим
public Person FindNewFriend() { return new Surgeon(); //or return new Plumber(); // or return new BabySitter(); }
Нам все равно, является ли человек врачом или водопроводчиком или няней, мы просто хотим выпить пива и хорошо провести время. Поэтому мы хотим поддерживать контравариантность в наших возвращаемых типах. Это успешно компилируется
Что мы не хотим
public BabySitter FindSomeoneToBabySit() { // We don't want just anybody to watch our child // We want an actual babysitter return new Person(); // This generates a compile error // What we want return new BabySitter(); }
Поэтому мы не хотим ковариации возвращаемого типа