В чем суть ковариантности и контравариантности делегатов?
1,00
р.
р.
Изучаю по книге работу с делегатами и есть там пример, объясняющий, что такое ковариантность и контравариантность. Решил подробнее поискать в гугле, но объяснений так и не нашел. В книге сказано, что ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата. К сожалению в практике это так разобрать и не смог, может кто сможет разжевать данный пример подробнее или преподнести свой, более легкий? 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, и вы используете его как список машин. Тогда, раз это список машин, в него можно добавить и Запорожец Lanos, правильно? Вот тут-то и начинаются проблемы. Если у вас в коде написано: List bmws = new List() List cars = bmws // поскольку список БМВ - это список машин cars.Add(new Lanos()) BMW bmw = bmvs[0] // ой. Внимательно посмотрите на этот код и подумайте над ним: он иллюстрирует проблему. (И он не откомпилируется: язык C# спроектирован так, чтобы не приводить к проблемам.) Проблема с записью в список. Если мы в список добавим произвольную машину, будет очень плохо: мы сможем нарушить гарантии, которые даёт нам система типов! Если бы у нас был список, доступный только на чтение, то проблем бы как раз не было: IEnumerable bmws = new List() { new BMW() } IEnumerable cars = bmws // а так можно //cars.Add(new Lanos()) // <-- не скомпилируется <br>Итак, что у нас получается? Несмотря на то, что BMW — машина, список BMW уже не обязательно является списком машин. А вот список BMW, доступный лишь на чтение, таки является списком машин. Есть? Теперь назад к вариантности. Мы говорим о ковариантности в общем смысле, если что-то меняется аналогичным образом. В случае наследования классов: мы можем вместо Car использовать BMW, и точно так же мы можем вместо IEnumerable использовать IEnumerable.
Окей, это было длинное вступление, теперь вернёмся к теме: ковариантность делегатов. Пусть у нас есть делегат, зависящий от типа 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 тоже.