Параметры-типы могут быть:
- Инвариантными. Параметр-тип не может изменяться.
- Контравариантными. Параметр-тип может быть
преобразован от класса к
классу, производному от него. В языке C# контравариантный тип
обозначается ключевым словом in. Контравариантный параметр-тип
может появляться только во входной позиции, например, в качестве
аргументов метода. - Ковариантными. Аргумент-тип может быть преобразован от класса к одному из его базовых классов. В языке С# ковариантный тип обозначается ключевым словом out. Ковариантный параметр обобщенного типа может появляться только в выходной позиции, например, в качестве возвращаемого значения метода.
Для начала, давайте глянем, что такое эта самая вариантность.
Пусть у нас есть два класса, 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]; // ой.
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()); // <-- не скомпилируется
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;
}
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;
}
BMW BmwFactory()
{
var bmw = new BMW();
bmw.EnginePower = 400;
return bmw;
}
// вы можете
использовать эту функцию как аргумент PrepareCar
// хотя её сигнатура другая:
return PrepareCar(BmwFactory);
// хотя её сигнатура другая:
return PrepareCar(BmwFactory);
Контравариантность работает в другую сторону: там вы можете
использовать делегат, работающий с базовым
типом там, где ожидается делегат с производным типом. Такое работает для
аргументов функций:
delegate double BmwTester(BMW bmw);
void TestAndPublish(BmwTester
tester)
{
var bmw = new BMW();
double testResult = tester(bmw);
PublishResult(testResult);
}
{
var bmw = new BMW();
double testResult = tester(bmw);
PublishResult(testResult);
}
double UniversalTester(Car
car)
{
return 5.0;
}
{
return 5.0;
}
// вы можете
использовать UniversalTester, хотя у него и не совсем подходящая сигнатура
TestAndPublish(UniversalTester);
TestAndPublish(UniversalTester);
Это работает по тем же причинам, что и ковариантность: если тестеру
подходит любой тип машины, то он сможет работать и с BMW тоже.
No comments:
Post a Comment