Изучаю по книге работу с делегатами и есть там пример, объясняющий, что такое ковариантность и контравариантность. Решил подробнее поискать в гугле, но объяснений так и не нашел.
В книге сказано, что ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата.
К сожалению в практике это так разобрать и не смог, может кто сможет разжевать данный пример подробнее или преподнести свой, более легкий?
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();
}
}
Для начала, давайте глянем, что такое эта самая вариантность.
Пусть у нас есть два класса, 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]; // ой.
Внимательно посмотрите на этот код и подумайте над ним: он иллюстрирует проблему. (И он не откомпилируется: язык C# спроектирован так, чтобы не приводить к проблемам.) Проблема с записью в список. Если мы в список добавим произвольную машину, будет очень плохо: мы сможем нарушить гарантии, которые даёт нам система типов!
Если бы у нас был список, доступный только на чтение, то проблем бы как раз не было:
IEnumerable<BMW> bmws = new List<BMW>() { new BMW() };
IEnumerable<Car> cars = bmws; // а так можно
//cars.Add(new Lanos()); // <-- не скомпилируется
Итак, что у нас получается? Несмотря на то, что 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 double BmwTester(BMW bmw);
void TestAndPublish(BmwTester tester)
{
var bmw = new BMW();
double testResult = tester(bmw);
PublishResult(testResult);
}
double UniversalTester(Car car)
{
return 5.0;
}
// вы можете использовать UniversalTester, хотя у него и не совсем подходящая сигнатура
TestAndPublish(UniversalTester);
Это работает по тем же причинам, что и ковариантность: если тестеру подходит любой тип машины, то он сможет работать и с BMW тоже.
Каждый из параметров-типов обобщенного делегата или интерфейса должен быть помечен как ковариантный или контравариантный. Это не приводит ни к каким нежелательным последствиям, но позволит применять ваших делегатов в большем количестве сценариев и позволит вам осуществлять приведение типа переменной обобщенного делегата к тому же типу делегата с другим параметром-типом.
Параметры-типы могут быть:
Предположим, что существует следующий тип делегата:
public delegate TResult MyDelegate<in T, out TResult>(T arg);
Здесь параметр-тип T
помечен словом in, делающим его контравариантным, а параметр-тип TResult
помечен словом out, делающим его ковариантным. Пусть объявлена следующая переменная:
MyDelegate<Object, ArgumentException> fn1 = null;
Ее можно привести к типу MyDelegate
с другими параметрами-типами:
MyDelegate<String, Exception> fn2 = fn1; // Явного приведения типа не требуется
Exception e = fn2("");
Это говорит о том, что fn1
ссылается на функцию, которая получает Object
и возвращает ArgumentException
. Переменная fn2
пытается сослаться на метод, который получает String
и возвращает Exception
. Так как мы можем передать String
методу, которому требуется тип Object
(тип String
является производным от Object
), а результат метода, возвращающего ArgumentException
, может интерпретироваться как Exception
(тип ArgumentException
является производным от Exception
), представленный здесь программный код откомпилируется, а на этапе компиляции будет сохранена безопасность типов.
Примечание Вариантность действует только в том случае, если компилятор сможет установить возможность преобразования ссылок между типами. Другими словами, вариантность неприменима для значимых типов из-за необходимости упаковки (boxing).
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Итак, имеется два класса: один родительский, второй дочерний
Есть два текстовых поля для ввода семизначного числа, где 1 поле: стартовое число, 2 поле: конечноеПодскажите, пожалуйста, как я могу получить...
Во время изменения фокуса из DataGrid на кнопку срабатывает обновление привязки у CurrentCell
Всем привет! Подскажите, как List, состоящий из String, перенести в определенную колонку DataGridViewС DataSource у меня не получилось, какие еще есть методы?