В чем суть ковариантности и контравариантности делегатов?

NaughtyBrain

Изучаю по книге работу с делегатами и есть там пример, объясняющий, что такое ковариантность и контравариантность. Решил подробнее поискать в гугле, но объяснений так и не нашел. В книге сказано, что ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата. К сожалению в практике это так разобрать и не смог, может кто сможет разжевать данный пример подробнее или преподнести свой, более легкий?

class X
{ public int Val;
}
class Y : X { }
//Этот делегат возвращает объект класса X и принимает объект класса Y в качестве аргумента
delegate X ChangeIt(Y obj);
class CoContraVariance
{ //Этот метод возвращает объект класса X и имеет объект класса X в качестве параметра static X IncrA(X obj) { X temp = new X(); temp.Val = obj.Val + 1; return temp; } //Этот метод возвращает объект класса Y и имеет объект класса Y в качестве параметра static Y IncrB(Y obj) { Y temp = new Y(); temp.Val = obj.Val + 1; return temp; } static void Main() { Y Yob = new Y(); ChangeIt change = IncrA; X Xob = change(Yob); Console.WriteLine("Xob: {0}", Xob.Val); change = IncrB; Yob = (Y)change(Yob); Console.WriteLine("Yob: {0}", Yob.Val); Console.ReadLine(); }
}
2 ответа

NaughtyBrain

Для начала, давайте глянем, что такое эта самая вариантность.

Пусть у нас есть два класса, Car и BMW. Очевидно, что BMW есть подкласс Car: каждая бэха является машиной.

Обычно при этом говорят так: «везде, где вы используете Car, можно использовать и BMW». Это на самом деле почти правда, но не совсем.

Пример: если у вас есть список машин, вы не можете вместо него использовать список BMW. Почему? А вот почему. Пускай вас есть List<BMW>, и вы используете его как список машин. Тогда, раз это список машин, в него можно добавить и Запорожец Lanos, правильно? Вот тут-то и начинаются проблемы. Если у вас в коде написано:

List<bmw> bmws = new List<bmw>();
List<car> cars = bmws; // поскольку список БМВ - это список машин
cars.Add(new Lanos());
BMW bmw = bmvs[0]; // ой.
</car></bmw></bmw>

Внимательно посмотрите на этот код и подумайте над ним: он иллюстрирует проблему. (И он не откомпилируется: язык C# спроектирован так, чтобы не приводить к проблемам.) Проблема с записью в список. Если мы в список добавим произвольную машину, будет очень плохо: мы сможем нарушить гарантии, которые даёт нам система типов!

Если бы у нас был список, доступный только на чтение, то проблем бы как раз не было:

IEnumerable<bmw> bmws = new List<bmw>() { new BMW() };
IEnumerable<car> cars = bmws; // а так можно
//cars.Add(new Lanos()); // <-- не скомпилируется
</car></bmw></bmw>

Итак, что у нас получается? Несмотря на то, что BMW — машина, список BMW уже не обязательно является списком машин. А вот список BMW, доступный лишь на чтение, таки является списком машин.

Есть?

Теперь назад к вариантности. Мы говорим о ковариантности в общем смысле, если что-то меняется аналогичным образом. В случае наследования классов: мы можем вместо Car использовать BMW, и точно так же мы можем вместо IEnumerable<Car> использовать IEnumerable<BMW>.

Окей, это было длинное вступление, теперь вернёмся к теме: ковариантность делегатов. Пусть у нас есть делегат, зависящий от типа Car. Поменяем в его определении Car на BMW, можно ли новый делегат использовать вместо старого?

Давайте рассуждать логически. Если у нас есть такой делегат:

public delegate Car Replace(Car original);

(он принимает на вход Car, и выдаёт другой экземпляр Car), то можно ли вместо него подставить функцию, описывающуюся делегатов такого вида:

public BMW MyReplace(BMW original) { ... }

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

А вот если наш вариантный тип данных (то есть, Car) находится лишь в позиции возвращаемого типа:

public delegate Car Create();

то на его месте можно использовать функцию такого вида:

public BMW CreateBmw() { ... }

(если подходила любая машина, то BMW тоже подойдёт).

Это и есть ковариантность делегатов: там, где от вас в коде требуется делегат, вы можете вместо него предоставить ковариантный делегат.

Пример кода, использующий это:

// это функция, принимающая делегат:
Car PrepareCar(Create carCreator)
{ Car car = carCreator(); car.ManufacturingDate = DateTime.Now; car.Mileage = 0; return car;
}
// это функция, которая ковариантна Create: она возвращает не Car, а BMW
BMW BmwFactory()
{ var bmw = new BMW(); bmw.EnginePower = 400; return bmw;
}
// вы можете использовать эту функцию как аргумент PrepareCar
// хотя её сигнатура другая:
return PrepareCar(BmwFactory);

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

delegate ****** BmwTester(BMW bmw);
void TestAndPublish(BmwTester tester)
{ var bmw = new BMW(); ****** testResult = tester(bmw); PublishResult(testResult);
}
****** UniversalTester(Car car)
{ return 5.0;
}
// вы можете использовать UniversalTester, хотя у него и не совсем подходящая сигнатура
TestAndPublish(UniversalTester);

Это работает по тем же причинам, что и ковариантность: если тестеру подходит любой тип машины, то он сможет работать и с BMW тоже.


NaughtyBrain

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

Параметры-типы могут быть:

  • Инвариантными. Параметр-тип не может изменяться.
  • Контравариантными. Параметр-тип может быть преобразован от класса к классу, производному от него. В языке C# контравариантный тип обозначается ключевым словом in. Контравариантный параметр-тип может появляться только во входной позиции, например, в качестве аргументов метода.
  • Ковариантными. Аргумент-тип может быть преобразован от класса к одному из его базовых классов. В языке С# ковариантный тип обозначается ключевым словом out. Ковариантный параметр обобщенного типа может появляться только в выходной позиции, например, в качестве возвращаемого значения метода.

Предположим, что существует следующий тип делегата:

public delegate TResult MyDelegate<in t, out tresult>(T arg);
</in t, out tresult>

Здесь параметр-тип T помечен словом in, делающим его контравариантным, а параметр-тип TResult помечен словом out, делающим его ковариантным. Пусть объявлена следующая переменная:

MyDelegate<object, argumentexception> fn1 = null;
</object, argumentexception>

Ее можно привести к типу MyDelegate с другими параметрами-типами:

MyDelegate<string, exception> fn2 = fn1; // Явного приведения типа не требуется
Exception e = fn2("");
</string, exception>

Это говорит о том, что fn1 ссылается на функцию, которая получает Object и возвращает ArgumentException. Переменная fn2 пытается сослаться на метод, который получает String и возвращает Exception. Так как мы можем передать String методу, которому требуется тип Object (тип String является производным от Object), а результат метода, возвращающего ArgumentException, может интерпретироваться как Exception (тип ArgumentException является производным от Exception), представленный здесь программный код откомпилируется, а на этапе компиляции будет сохранена безопасность типов.

Примечание Вариантность действует только в том случае, если компилятор сможет установить возможность преобразования ссылок между типами. Другими словами, вариантность неприменима для значимых типов из-за необходимости упаковки (boxing).

licensed under cc by-sa 3.0 with attribution.