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

Вариантность — это то, что описывает возможность преобразования и способность делать то, что имеет смысл для типа. Например, рассмотрим следующий код, где демонстрируется ковариантность массивов, которая была возможной в С# со времен версии 1.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
static class EntryPoint
{
static void Main()  {
string [] strings = new string []  { "One", "Two", "Three"};
DisplayStrings( strings ) ;
// Правила ковариантности массивов
// допускают следующее присваивание
object[] objects = strings;
// Но что теперь произойдет?
objects[1] = new object ();
DisplayStrings( strings ) ;
}
static void DisplayStrings ( string[] strings )  {
Console.WriteLine ( "-----Вывод строк-----" );
foreach ( var s in strings )  {
Console.WriteLine ( s );
}

В начале метода Main создается массив строк, который затем немедленно передается в DisplayStrings для его вывода на консоль. После этого переменной типа objects [ ] присваивается значение переменной strings. Поскольку strings и objects являются переменными ссылочных типов, на первый взгляд логично иметь возможность присвоить strings переменной objects, так как тип string неявно преобразуем в тип object.

Однако обратите внимание, что сразу после этого первый элемент objects был заменен некоторым экземпляром object. Что произойдет в результате второго вызова DisplayStrings с передачей ему массива strings? Как и можно было догадаться, исполняющая система сгенерирует исключение типа ArrayTypeMismatchException:

Unhandled Exception: System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.

Необработанное исключение: System.ArrayTypeMismatchException: Попытка обращения к элементу с типом, который несовместим с массивом.

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

То есть, если массив инвариантен, подобно System.String, то в точке, где он присваивается другой переменной, происходит копирование обычным образом, т.е. в “ленивой” манере. Однако давайте посмотрим, как можно решить эту проблему с использованием обобщений:

1
2
3
4
5
6
7
8
9
using System;
using System.Collections.Generic;
static class EntryPoint
{
static void Main()  {
List<string> strings = new List<string> { "One", "Two", "Three" };
// Это не скомпилируете*! ! !
List<object> objects = strings;
}

Приведенный фрагмент кода по духу идентичен примеру с ковариантностью массивов. Тем не менее, он не скомпилируется. Компилятор выдаст следующее сообщение об ошибке:

1
2
3
error CS0029: Cannot implicitly convert type 1 System.Collections.Generic.List<string>1 to 1 System.Collections.Generic.List<object>1
 
ошибка CS0029: He удается неявно преобразовать тип System.Collections.Generic.List<string> в System.Collections.Generic.List<object>

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

1
List<string> и List<object>

не существует, и только то, что оба они сконструированы на основе List, a string неявно преобразуется в object, не означает, что они преобразуемы друг в друга.

Однако не теряйте надежды. В С# 4.0 появился синтаксис, который позволяет достигнуть нужного результата. Используя этот новый синтаксис, можно обозначить, что обобщенный интерфейс или делегат поддерживает ковариантность или контравариант-ность. Вдобавок новые правила вариантности касаются только сконструированных типов, которым ссылочные типы передаются в виде аргументов обобщенного типа.

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

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

1
2
3
4
string s = "Hello";
object о = s;
string[] strings = new string[3];
object[] objects = strings;

Первые две строки имеют четкий смысл: в конечном итоге, переменные типа string неявно преобразуемы в тип object, поскольку тип string унаследован от object. Второй набор строк показывает, что переменные типа string [ ] неявно преобразуемы в переменные типа object [ ].

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

Теперь, чтобы транслировать эту концепцию в область присваивания обобщенных интерфейсов, интерфейс типа IOperation является ковариантно преобразуемым в I0peration, если существует неявное преобразование ссылок от Т к R и от I0peration к I0peration.

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

1
2
Т —> R
IOperation<t> -> IOperation<r>

Правила вариантности С# неприменимы к типам значений, т.е. к типам, которые не преобразуются в ссылки. Другими словами, тип IOperation не является ковариантно преобразуемым в IOperation, даже несмотря на то, что int неявно преобразуется в double.

Давайте рассмотрим пример, в котором имеется специальная коллекция по имени MyCollection, реализующая интерфейс IMyCollection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections.Generic;
interface IMyCollection<t>
{
void Addltem( T item ) ;
T Getltem( int index );
}
class MyCollection<t> : IMyCollection<t> {
public void Addltem( T item )  {
collection.Add( item );
}
public T Getltem( int index )  {
return collection[index];
}
private List<t> collection = new List<t>();
}
static class EntryPoint
{
static void Main()  {
var strings = new MyCollection<string>();
strings.Addltem( "One" );
strings.Addltem( "Two" );
IMyCollection<string> collStrings = strings;
PrintCollection ( collStrings, 2 );
}
static void PrintCollection ( IMyCollection<string> coll, int count )  {
for ( int i = 0;
i < count; 
++i )  {
Console.WriteLine ( coll.Getltem(i) );
}

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

Но теперь давайте предположим, что метод PrintCollection должен принимать экземпляр типа

1
MyCollection<object> вместо MyCollection<string>

В конце концов, логично, что коллекция string также является коллекцией object.

Если просто изменить сигнатуру PrintCollection для приема

1
MyCollection<object>

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

Необходимо что-то еще. Удостоверьтесь, что приведенная ниже модификация кода компилируется и работает ожидаемым образом. Заслуживающие внимания отличия выделены полужирным.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using System.Collections.Generic-interface IMyCollection<t> {
void Addltem ( T item ) ;
}
interface IMyEnumerator<out T> {
T Getltem( int index ) ;
}
class MyCollection<t> : IMyCollection<t>, IMyEnumerator<t> {
public void Addltem ( T item )  {
collection.Add( item );
}
public T Getltem( int index )  {
return collection[index];
}
private List<t> collection = new List<t>();
}
static class EntryPoint
{
static void Main()  {
var strings = new MyCollection<string>() ;
strings.Addltem( "One" );
strings.Addltem( "Two" );
IMyEnumerator<string> collStrings = strings;
// Ковариантность в действии!
IMyEnumerator<object> collObjects = collStrings;
PrintCollection ( collObjects, 2 );
}
static void PrintCollection ( IMyEnumerator<object> coll, int count )  {
for ( int i = 0;
i < count; 
++i )  {
Console.WriteLine ( coll.Getltem(i)  );
}

Для начала обратите внимание, что предыдущая реализация IMyCollection разделена на два интерфейса с именами IMyCollection и IMyEnumerator. Объяснения будут даны ниже. Также обратите внимание, что PrintCollection принимает переменную типа

1
IMyEnumerator<object> вместо IMyEnumerator<string>

. Но что более важно, присмотритесь внимательнее к объявлению IMyEnumerator, особенно к способу декорирования параметра обобщения с помощью ключевого слова out.

Ключевое слово out в списке параметров обобщения — это способ указания, что обобщенный интерфейс ковариантен в Т. Другими словами, подобным образом компилятору сообщается, что если R неявно преобразуется в S, то также и

1
ImyEnumerator<r> неявно преобразуется в ImyEnumerator<s>

Почему ключевое слово названо out?

Потому, что так случилось, что обобщенные интерфейсы, которые ковариантны в Т, обычно имеют Т в выходной позиции принадлежащих им методов. Теперь должно быть понятно, почему первоначальный интерфейс IMyCollection был разделен на два интерфейса — потому что метод IMyCollection.Addltem не содержит Т в выходной позиции.

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

С выходом С# 4.0 давно известные типы IEnumerable и IEnymerator обозначены как ковариантные с помощью ключевого слова out. Это очень помогает, особенно при работе с LINQ.

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

Как и можно было ожидать, контравариантность — это противоположность ковариантности. То есть для присваивания обобщенного интерфейса интерфейс типа IOperation контравариантно преобразуем в IOperation, если существует неявное преобразование из R в Т и из IOperation в IOperation.

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

1
2
R —> Т
IOperation<t> -> Ioperation<r>

Контравариантные обобщенные параметры в обобщенных интерфейсах и делегатах помечаются с использованием новой декорации обобщенных параметров in. Чтобы проиллюстрировать это, давайте вернемся к надуманному классу MyCollection из предыдущего раздела, и предположим, что нужна возможность удаления элемента из коллекции. Интересующие фрагменты кода выделены полужирным.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic; class A { } class В : A { }
interface IMyCollection<t> void Addltem ( T item ) ;
interface IMyTrimmableCollection<in T> void Remove Item ( T item ) ;
class MyCollection<t> : IMyCollection<t>, IMyTrimmableCollection<t>
public void Addltem ( T item )  { collection.Add( item ); }
public void RemoveItem( T item )  { collection.Remove ( item ); }
private List<t> collection = new List<t>();
}
static class EntryPoint {
static void Main()  {
var items = new MyCollection<a>();
items.Addltem ( new A() ) ;
В b = new В () ;
items.Addltem( b );
IMyTrimmableCollection<a> collltems = items;
// Контравариантность в действии!
IMyTrimmableCollection<b> trimColl = collltems;
trimColl.Remove I tern ( b ) ;
}

Чтобы можно было сосредоточиться исключительно на случае с контравариантно-стью, некоторый код из примера ковариантности удален. Обратите внимание на использование ключевого слова in в объявлении интерфейса IMyTrimmableCollection.

Это говорит компилятору, что в соответствии с необходимой операцией (усечение в данном примере) существует неявное контравариантное преобразование из

1
ImyTrimmableCollection<a> в ImyTrimmableCollection<b>

, поскольку имеется неявное контравариантное преобразование из В в А.

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

1
MyCollection<a>

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

До настоящего момента примеры ковариантности и контравариантности были показаны с использованием модификации одного и того же выдуманного класса коллекций. При этом перечисление на коллекции было ковариантным, а удаление из коллекции — контравариантным. Но как насчет добавления элементов в коллекцию?

Какую разновидность вариантности представляет оно? Уже доступен интерфейс IMyCollection, который для удобства повторяется ниже:

1
2
3
4
interface IMyCollection<t>
{
void Addltem ( T item ) ;
}

Поскольку имеется ссылка на

1
IMyCollection<a>

, значит, должна существовать возможность добавления экземпляров В, так как В наследуется от А. Поэтому вызов Addltem на

1
IMyCollection<a>

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

1
IMyCollection<a>

с передачей экземпляра В.

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

1
IMyCollection<b> преобразуем в IMyCollection<a>

, то операция контравариантна.

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

1
2
3
4
interface IMyCollection<in Т>
{
void Addltem( Т item );
}

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

Обобщенный тип интерфейса или делегата, где параметры обобщения не декорированы вовсе, является инвариантным. Естественно, все такие интерфейсы и делегаты были инвариантными и до появления С# 4.0, потому что не существовало декораций in и out для параметров обобщений. Как было показано в предыдущем разделе, наш надуманный интерфейс IMyCollection выглядит так:

1
2
3
4
5
interface IMyCollection<t>
{
void Addltem ( T item ) ;
T Getltem( int index ) ;
}

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

То есть, можно было бы компилировать код, в котором разрешено добавлять в коллекцию несовместимые типы. Почему так? Представим на мгновение, что показанный выше интерфейс мог быть помечен следующим образом:

1
2
3
4
5
6
// Это не будет работать!
interface IMyCollection<out Т>
{
void Addltem ( Т item ) ;
T Getltem( int index );
}

Затем, основываясь на определении ковариантности, переменную типа IMyCollection можно было бы присваивать переменной типа IMyCollection. И тогда через последнюю переменную удалось бы сделать нечто подобное показанному ниже:

1
2
3
4
// Ничего кроме вреда!
MyCollection<string> strings = . . .;
IMyCollection<object> objects = strings;
objects.Addltem( new MonkeyWrench() );

Таким образом, большая часть головной боли, связанной с инвариантностью массивов в С#, избегается за счет использования обобщений, которые объединены с синтаксисом, добавленным в С# 4.0. Другими словами, правила вариантности для обобщений обеспечивают безопасности типов, в то время как правила вариантности для простых старых массивов — нет.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100