Скоро (22 марта 2010)  выходит Visual Studio 2010, которая будет поддерживать C# 4.0 Больше всего вопросов возникает из-за новой фичи языка – ко- и контр- вариантности. Попытаюсь дать объяснение на человеческом языке.

В любой системе типов существуют отношения между типами. Нас интересует отношение типа-подтип. Для типов A и B будем обозначать A :> B, если A является подтипом B (B является супертипом A). Если A является подтипом B, то везде где в программе требуется значение типа B можно подставлять значение типа A без каких-либо дополнительных конструкций.

Например во многих языках тип целых чисел является подтипом вещественных. В ОО-языках такое отношение реализуется за счет наследования. Если A является наследником B, то A является подтипом B.

Тут стоит вспомнить принцип LSP (принцип подстановки Барбары Лисков). Он ошибочно приписывается к ООП, хотя имеет к нему весьма отдаленное отношение. Принцип гласит что если A :> B, то любое утверждение для B должно быть верно для A. Выполнение этого принципа означает что поведение программы при подстановке значения типа A там где требуется B не изменится.

Но это я ушел в сторону.  Когда у нас чистый ООП язык (как smalltalk) тогда отношения типов-подтипов исчерпываются наследованием, которое создает довольно простые отношения. Все становится сложно когда появляются типы, параметризуемые другими типами (обобщенные типы).

Будем обозначать обобщенный типа как T<`a>, где `a – параметр типа. Конкретный тип при подстановке параметра будем обозначать T<A>, где A – какой-то тип. Для иллюстраций нам понадобится обобщенный тип с одним параметром, хотя типов-параметром может быть много.

Тут возникает интересный вопрос. Если A :> B, то как связаны T<A> и  T<B> ?

Тип T<`a> называется ковариантными, если для A :> B выполняется T<A> :> T<B>, и контрвариантым, если A :> B выполняется T<B> :> T<A>,
если же T<A> и  T<B> не связаны никакими отношениями, то такой тип называет инвариантым.

Примеры.

1)IEnumerable<T>. Например если Apple унаследован от Fruit (то есть Apple :> Fruit), то вполне резонно было бы иметь IEnumerable<Apple> :> IEnumerable<Fruit>. Действительно, в .NET 4 IEnumerable<T> является ковариантым и имеет сигнатуру IEnumerable<out T>.

2)Action<T>. Например есть метод void Eat(Fruit f), он имеет тип Action<Fruit>, и у нас Apple :> Fruit. Тогда было бы хорошо иметь Action<Fuit> :> Action<Apple>, то есть если нам куда-то понадобится передавать Action<Apple> мы могли бы туда передать Action<Fruit>. В .NET 4 Action<T> является конртвариантным и имеет сигнатуру Action<in T>.

Магические слова in и out.

Такие модификаторы были выбраны неслучайно. Ко- и контр- вариантность может приводить к ошибкам при неумелом использовании. Например массивы в .NET 2 и выше являются ковариантными. То есть там где требуется Fruit[] можно передать Apple[]. Но программист может внутри метода, обрабатывающего Fruit[] присвоить элементу массива значение типа Banana. Что приведет к runtime error.

Чтобы ковариантность была безопасной необходимо чтобы ковариантные типы-аргументы были только в выходных значениях методов. То есть для T<out `a> можно писать методы возвращающие `a или имеющие out-параметры типа `a. также могут быть get-only свойства, возвращающие `a.

Аналогично для контрвариантного T<in `a> параметры типа `a могут быть только во входных параметрах методов.

Темная сторона силы.

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

PS. В C# отношение тип-подтип проверяется оператором is.