Разница между ковариацией и контрастностью

У меня проблемы с пониманием разницы между ковариацией и контравариантностью.

8 ответов

Решение

Вопрос в том, "в чем разница между ковариацией и контравариантностью?"

Ковариантность и контравариантность являются свойствами отображающей функции, которая связывает один член набора с другим. Более конкретно, отображение может быть ковариантным или контравариантным относительно отношения на этом множестве.

Рассмотрим следующие два подмножества множества всех типов C#. Первый:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

И во-вторых, это явно связанный набор:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

Существует операция отображения из первого набора во второй набор. То есть для каждого T в первом наборе соответствующий тип во втором наборе IEnumerable<T>, Или, в кратком виде, отображение T → IE<T>, Обратите внимание, что это "тонкая стрела".

Со мной так далеко?

Теперь давайте рассмотрим отношение. Между парами типов в первом наборе существует отношение совместимости присваивания. Значение типа Tiger может быть назначен переменной типа Animalпоэтому эти типы называются "совместимыми по присваиванию". Давайте напишем "значение типа X может быть назначен переменной типа Y"в более короткой форме: X ⇒ Y, Обратите внимание, что это "жирная стрела".

Итак, в нашем первом подмножестве приведены все отношения совместимости присвоений:

Tiger  ⇒ Tiger
Tiger  ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit  ⇒ Fruit

В C# 4, который поддерживает ковариантную совместимость присваивания определенных интерфейсов, существует отношение совместимости присваивания между парами типов во втором наборе:

IE<Tiger>  ⇒ IE<Tiger>
IE<Tiger>  ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit>  ⇒ IE<Fruit>

Обратите внимание, что отображение T → IE<T> сохраняет существование и направление присваивания совместимости. То есть если X ⇒ Yто верно и то, что IE<X> ⇒ IE<Y>,

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

Отображение, которое обладает этим свойством в отношении определенного отношения, называется "ковариантным отображением". Это должно иметь смысл: последовательность Тигров может использоваться там, где необходима последовательность Животных, но обратное неверно. Последовательность животных не обязательно может быть использована там, где необходима последовательность тигров.

Это ковариация. Теперь рассмотрим это подмножество множества всех типов:

{ IComparable<Tiger>, 
  IComparable<Animal>, 
  IComparable<Fruit>, 
  IComparable<Banana> }

Теперь у нас есть отображение из первого набора в третий набор T → IC<T>,

В C# 4:

IC<Tiger>  ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger>     Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit>  ⇒ IC<Banana>     Backwards!
IC<Fruit>  ⇒ IC<Fruit>

То есть отображение T → IC<T> сохранил существование, но поменял направление совместимости присваивания. То есть если X ⇒ Y, затем IC<X> ⇐ IC<Y>,

Отображение, которое сохраняет, но обращает отношение, называется контравариантным отображением.

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

Так что в C# 4 разница между ковариацией и контравариантностью. Ковариация сохраняет направление присваиваемости. Контравариантность полностью изменяет это.

Привести примеры, наверное, проще всего - я их точно помню.

ковариации

Канонические примеры: IEnumerable<out T>, Func<out T>

Вы можете конвертировать из IEnumerable<string> в IEnumerable<object>, или же Func<string> в Func<object>, Значения только выходят из этих объектов.

Это работает, потому что, если вы берете значения только из API, и он собирается возвращать что-то конкретное (например, string), вы можете рассматривать это возвращаемое значение как более общий тип (например, object).

контрвариация

Канонические примеры: IComparer<in T>, Action<in T>

Вы можете конвертировать из IComparer<object> в IComparer<string>, или же Action<object> в Action<string>; значения входят только в эти объекты.

На этот раз это работает, потому что, если API ожидает чего-то общего (например, object) вы можете дать ему что-то более конкретное (например, string).

В более общем смысле

Если у вас есть интерфейс IFoo<T> это может быть ковариантным в T (т.е. объявить это как IFoo<out T> если T используется только в выходной позиции (например, тип возврата) в интерфейсе. Это может быть противоречивым в T (т.е. IFoo<in T>) если T используется только в позиции ввода (например, тип параметра).

Это может привести к путанице, потому что "позиция вывода" не так проста, как кажется - параметр типа Action<T> все еще только использует T в выходной позиции - противоположность Action<T> переворачивает, если вы понимаете, о чем я. Это "выход" в том смысле, что значения могут передаваться от реализации метода к коду вызывающей стороны, так же как и возвращаемое значение. Обычно такие вещи не подходят, к счастью:)

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

Для наших внутренних тренингов я работал с замечательной книгой "Smalltalk, Objects and Design (Chamond Liu)" и перефразировал следующие примеры.

Что означает "согласованность"? Идея состоит в том, чтобы спроектировать безопасные с точки зрения типов иерархии типов с легко заменяемыми типами. Ключом к получению этой согласованности является соответствие на основе подтипов, если вы работаете на языке со статической типизацией. (Мы обсудим принцип замены Лискова (LSP) здесь на высоком уровне.)

Практические примеры (псевдокод / ​​неверный в C#):

  • Ковариантность: Давайте предположим, что птицы, которые откладывают яйца "последовательно" со статической типизацией: если тип Bird откладывает яйцо, не подтип Bird подкладывает подтип яйца? Например, тип Duck откладывает DuckEgg, затем задается последовательность. Почему это соответствует? Потому что в таком выражении:Egg anEgg = aBird.Lay();ссылка aBird может быть юридически заменена на Bird или на экземпляр Duck. Мы говорим, что возвращаемый тип является ковариантным типу, в котором определен Lay(). Переопределение подтипа может возвращать более специализированный тип. => "Они доставляют больше."

  • Contravariance: Давайте предположим, что пианино, что пианисты могут играть "последовательно" со статической типизацией: если пианист играет на пианино, сможет ли она играть на рояле? Разве виртуоз не хотел бы играть на рояле? (Будьте осторожны; есть поворот!) Это противоречиво! Потому что в таком выражении: aPiano.Play(aPianist); aPiano не может быть юридически заменено фортепиано или экземпляром GrandPiano! Играть на рояле может только виртуоз, пианисты слишком общие! GrandPianos должны воспроизводиться более общими типами, тогда игра будет последовательной. Мы говорим, что тип параметра противоположен типу, в котором определен Play(). Переопределение подтипа может принимать более обобщенный тип. => "Они требуют меньше."

Вернуться к C#:
Поскольку C# в основном является языком со статической типизацией, "местоположения" интерфейса типа, которые должны быть ко-или контравариантными (например, параметры и возвращаемые типы), должны быть помечены явно, чтобы гарантировать последовательное использование / развитие этого типа, чтобы сделать LSP работает нормально. В динамически типизированных языках согласованность LSP обычно не является проблемой, другими словами, вы могли бы полностью избавиться от ко-и контравариантной "разметки" в.Net-интерфейсах и делегатах, если бы вы использовали только динамический тип в ваших типах. - Но это не лучшее решение в C# (вы не должны использовать динамические в публичных интерфейсах).

Вернуться к теории:
Описанное соответствие (ковариантные возвращаемые типы / контравариантные типы параметров) является теоретическим идеалом (поддерживаемым языками Emerald и POOL-1). Некоторые языки oop (например, Eiffel) решили применить другой тип согласованности, особенно также ковариантные типы параметров, потому что он лучше описывает реальность, чем теоретический идеал. В статически типизированных языках желаемая согласованность часто должна достигаться путем применения шаблонов проектирования, таких как "двойная диспетчеризация" и "посетитель". Другие языки предоставляют так называемые "множественные диспетчеризации" или множественные методы (это, в основном, выбор перегрузки функций во время выполнения, например, с CLOS), или получают желаемый эффект с помощью динамической типизации.

Дисперсия Co и Contra - довольно логичные вещи. Система языковых типов заставляет нас поддерживать логику реальной жизни. Это легко понять на примере.

Ковариация

Например, вы хотите купить цветок, и в вашем городе есть два цветочных магазина: магазин роз и магазин ромашек.

Если вы спросите кого-нибудь, "где находится цветочный магазин?" и кто-нибудь скажет вам, где находится магазин роз, можно? Да, поскольку роза - это цветок, если вы хотите купить цветок, вы можете купить розу. То же самое касается и тех, кто ответил вам адресом магазина ромашек.

Это пример ковариации: вам разрешено использоватьA<C> к A<B>, где C является подклассом B, если Aпроизводит общие значения (возвращается в результате функции). Ковариация касается производителей, поэтому C# использует ключевое словоout для ковариации.

Типы:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

На вопрос "где находится цветочный магазин?", Ответ "там магазин роз":

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

Контравариантность

Например, вы хотите подарить цветок своей девушке, и ваша девушка любит любые цветы. Можете ли вы считать ее человеком, который любит розы, или человеком, который любит ромашки? Да, потому что, если она любит любой цветок, она полюбит и розу, и ромашку.

Это пример контравариантности: вам разрешено использоватьA<B> к A<C>, где C является подклассом B, если Aпотребляет общую ценность. Контравариантность касается потребителей, поэтому C# использует ключевое словоin для контравариантности.

Типы:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

Вы рассматриваете свою девушку, которая любит любой цветок, как человека, любящего розы, и дарите ей розу:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Ссылки

Делегат конвертера помогает мне понять разницу.

delegate TOutput Converter<in TInput, out TOutput>(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<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();

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

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

Лучше всего, если я объясню, почему ковариация возвращаемого типа и контравариантность входных параметров не разрешены в C#.

Хирург, BabySitter и Plumber являются производными от класса 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();
    }

Поэтому мы не хотим возвращать ковариацию типа

Представьте, что в организации есть две должности. Алиса - это стойка для стульев. И Боб - кладовщик таких же стульев.

Контравариантность. Теперь мы не можем назвать Боба кладовщиком мебели, потому что он не берет стол в свой магазин, он хранит только стулья. Но мы можем назвать его кладовщиком фиолетовых стульев, потому что фиолетовый - это стул. Это , мы разрешаем отнесение к более конкретным типам, а не к меньшим. означает потоки данных в объект.

Covarinace. Напротив, мы можем назвать Алису прилавком мебели, потому что это не повлияет на ее роль. Но мы не можем назвать ее прилавком красных стульев, потому что мы ожидаем, что она не считает не красные стулья, а она их считает. Это , разрешить неявное преобразование в менее конкретное, а не в более конкретное. означает потоки данных из объекта.

А инвариантность - это когда мы не можем сделать и то, и другое.

Другие вопросы по тегам