Каковы виды ковариации в С#? (Или, ковариация: пример)

Ковариация - это (примерно) способность зеркального наследования "простых" типов в сложных типах, которые их используют.
Например. Мы всегда можем рассматривать экземпляр Cat как экземпляр Animal. A ComplexType<Cat> можно рассматривать как a ComplexType<Animal>, если ComplexType является ковариантным.

Мне интересно: что такое "типы" ковариации и как они относятся к С# (поддерживаются ли они?)
Примеры кода были бы полезны.

Например, один тип - это ковариация возвращаемого типа, поддерживаемая Java, но не С#.

Я надеюсь, что кто-то с функциональными программирующими отбивками тоже может перезвонить!

Ответ 1

Вот о чем я могу думать:

Обновление

После прочтения конструктивных комментариев и тонны статей, обозначенных (и написанных) Эриком Липпертом, я улучшил ответ:

  • Обновлен сломанность ковариации массива
  • Добавлена ​​ "чистая" дисперсия делегатов.
  • Добавлено больше примеров из BCL
  • Добавлены ссылки на статьи, в которых подробно объясняются понятия.
  • Добавлен целый новый раздел ковариации параметров функции более высокого порядка.

Ковариация возвращаемого типа:

Доступно на Java ( >= 5) [1] и С++ [2] не поддерживается в С# (Эрик Липперт объясняет почему бы и нет и что вы можете с этим сделать):

class B {
    B Clone();
}

class D: B {
    D Clone();
}

Ковариация интерфейса [3] - поддерживается в С#

BCL определяет общий IEnumerable интерфейс, который должен быть ковариантным:

IEnumerable<out T> {...}

Таким образом, справедлив следующий пример:

class Animal {}
class Cat : Animal {}

IEnumerable<Cat> cats = ...
IEnumerable<Animal> animals = cats;

Обратите внимание, что IEnumerable по определению "только для чтения" - вы не можете добавлять к нему элементы.
Сравните это с определением IList<T>, которое может быть изменено, например. используя .Add():

public interface IEnumerable<out T> : ...  //covariant - notice the 'out' keyword
public interface IList<T> : ...            //invariant

Ковариация делегатов с помощью групп методов [4] - поддерживается в С#

class Animal {}
class Cat : Animal {}

class Prog {
    public delegate Animal AnimalHandler();

    public static Animal GetAnimal(){...}
    public static Cat GetCat(){...}

    AnimalHandler animalHandler = GetAnimal;
    AnimalHandler catHandler = GetCat;        //covariance

}

"Чистая" ковариация делегатов [5 - pre-variance-release article] - поддерживается в С#/p >

Определение BCL делегата, которое не принимает никаких параметров и что-то возвращает, ковариантно:

public delegate TResult Func<out TResult>()

Это позволяет:

Func<Cat> getCat = () => new Cat();
Func<Animal> getAnimal = getCat; 

Ковариация массивов - поддерживается на С# сломанным способом [6][7]

string[] strArray = new[] {"aa", "bb"};

object[] objArray = strArray;    //covariance: so far, so good
//objArray really is an "alias" for strArray (or a pointer, if you wish)


//i can haz cat?
object cat == new Cat();         //a real cat would object to being... objectified.

//now assign it
objArray[1] = cat                //crash, boom, bang
                                 //throws ArrayTypeMismatchException

И, наконец, удивительный и немного умопомрачительный Ковариация параметров делегата (да, эта ковариантность) - для функций более высокого порядка. [8]

Определение BCL делегата, которое принимает один параметр и ничего не возвращает, контравариантно:

public delegate void Action<in T>(T obj)

Медведь со мной. Пусть определит циркового тренера-животного - ему можно рассказывать, как обучать животное (давая ему Action, который работает с этим животным).

delegate void Trainer<out T>(Action<T> trainingAction);

У нас есть определение тренера, дайте тренеру и поместите его на работу.

Trainer<Cat> catTrainer = (catAction) => catAction(new Cat());

Trainer<Animal> animalTrainer = catTrainer;  
// covariant: Animal > Cat => Trainer<Animal> > Trainer<Cat> 

//define a default training method
Action<Animal> trainAnimal = (animal) => 
   { 
   Console.WriteLine("Training " + animal.GetType().Name + " to ignore you... done!"); 
   };

//work it!
animalTrainer(trainAnimal);

Результат доказывает, что это работает:

Обучение коту игнорировать вас... сделано!

Чтобы понять это, шутка в порядке.

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

Голос из задней части комнаты подпрыгнул:" Да, правильно".

Что это связано с ковариацией?!

Позвольте мне попробовать демонстрацию салфетки.

An Action<T> является контравариантным, т.е. "переворачивает" отношения типов:

A < B => Action<A> > Action<B> (1)

Измените A и B выше с помощью Action<A> и Action<B> и получите:

Action<A> < Action<B> => Action<Action<A>> > Action<Action<B>>  

or (flip both relationships)

Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (2)     

Положим (1) и (2) вместе и имеем:

,-------------(1)--------------.
 A < B => Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (4)
         `-------------------------------(2)----------------------------'

Но наш Trainer<T> делегат эффективно Action<Action<T>>:

Trainer<T> == Action<Action<T>> (3)

Итак, мы можем переписать (4) как:

A < B => ... => Trainer<A> < Trainer<B> 

- который, по определению, означает, что тренер является ковариантным.

Короче говоря, применяя Action дважды, мы получаем contra-contra-variance, т.е. отношение между типами переворачивается дважды (см. (4)), поэтому мы возвращаемся к ковариации.

Ответ 2

Это лучше всего объяснить с точки зрения более общих структурных типов. Рассмотрим:

  • Типы кортежей: (T1, T2), пара типов T1 и T2 (или, более общо, n-кортежей);
  • Типы функций: T1 → T2, функция с типом аргумента T1 и результатом T2;
  • Mutable types: Mut (T), изменяемая переменная, содержащая T.

Кортежи ковариантны в обоих их типах компонентов, т.е. (T1, T2) (U1, U2), если T1 < U1 и T2 < U2 (где '<' означает is-subtype-of).

Функции ковариантны по своим результатам и контравариантны по своим аргументам, т.е. (T1 → T2) (U1 → U2), если U1 < T1 и T2 < U2.

Переменные типы являются инвариантными, т.е. Mut (T) Mut (U) только если T = U.

Все эти правила являются наиболее общими правильными правилами подтипирования.

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

interface C<T, U, V> {
  T f(U, U)
  Int g(U)
  Mut(V) x
}

по существу представляет тип

C(T, U, V) = ((U, U) -> T, U -> Int, Mut(V))

где f, g и x соответствуют 1-му, 2-му и 3-му компонентам кортежа соответственно.

Из приведенных выше правил следует, что C (T, U, V) C (T ', U', V '), если T < T 'и U' U и V = V '. Это означает, что общий тип C ковариантен в T, контравариантный в U и инвариантен относительно V.

Другой пример:

interface D<T> {
  Int f(T)
  T g(Int)
}

является

D(T) = (T -> Int, Int -> T)

Здесь D (T) D (T '), только если T < T 'и T' T. В общем случае это может быть только в случае, если T = T ', поэтому D фактически инвариантно относительно T.

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

interface E<T> { Int f(Int) }

является бивариантным в T, потому что он фактически не используется.

Ответ 3

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