Ковариация и контравариация пример реального мира
У меня возникли небольшие проблемы с пониманием того, как я буду использовать ковариацию и контравариантность в реальном мире.
до сих пор единственные примеры, которые я видел, были тем же самым старым примером массива.
object[] objectArray = new string[] { "string 1", "string 2" };
было бы неплохо увидеть пример, который позволил бы мне использовать его во время моей разработки, если бы я мог видеть, что он используется в другом месте.
9 ответов:
допустим, у вас есть классный человек и класс, который происходит от него, учитель. У вас есть некоторые операции, которые принимают
IEnumerable<Person>
в качестве аргумента. В вашей школе у вас есть метод, который возвращаетIEnumerable<Teacher>
. Ковариация позволяет непосредственно использовать этот результат для методов, которые принимаютIEnumerable<Person>
, подставляя более производный тип для менее производного (более общего) типа. Контравариантность, контр-интуитивно, позволяет использовать более общий тип, где указан более производный тип. См. также https://msdn.microsoft.com/en-us/library/dd799517.aspxpublic class Person { public string Name { get; set; } } public class Teacher : Person { } public class MailingList { public void Add(IEnumerable<out Person> people) { ... } } public class School { public IEnumerable<Teacher> GetTeachers() { ... } } public class PersonNameComparer : IComparer<Person> { public int Compare(Person a, Person b) { if (a == null) return b == null ? 0 : -1; return b == null ? 1 : Compare(a,b); } private int Compare(string a, string b) { if (a == null) return b == null ? 0 : -1; return b == null ? 1 : a.CompareTo(b); } } ... var teachers = school.GetTeachers(); var mailingList = new MailingList(); // Add() is covariant, we can use a more derived type mailingList.Add(teachers); // the Set<T> constructor uses a contravariant interface, IComparer<T>, // we can use a more generic type than required. See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());
// Contravariance interface IGobbler<in T> { void gobble(T t); } // Since a QuadrupedGobbler can gobble any four-footed // creature, it is OK to treat it as a donkey gobbler. IGobbler<Donkey> dg = new QuadrupedGobbler(); dg.gobble(MyDonkey()); // Covariance interface ISpewer<out T> { T spew(); } // A MouseSpewer obviously spews rodents (all mice are // rodents), so we can treat it as a rodent spewer. ISpewer<Rodent> rs = new MouseSpewer(); Rodent r = rs.spew();
для полноты...
// Invariance interface IHat<T> { void hide(T t); T pull(); } // A RabbitHat… IHat<Rabbit> rHat = RabbitHat(); // …cannot be treated covariantly as a mammal hat… IHat<Mammal> mHat = rHat; // Compiler error // …because… mHat.hide(new Dolphin()); // Hide a dolphin in a rabbit hat?? // It also cannot be treated contravariantly as a cottontail hat… IHat<CottonTail> cHat = rHat; // Compiler error // …because… rHat.hide(new MarshRabbit()); cHat.pull(); // Pull a marsh rabbit out of a cottontail hat??
вот что я собрал, чтобы помочь мне понять разницу
public interface ICovariant<out T> { } public interface IContravariant<in T> { } public class Covariant<T> : ICovariant<T> { } public class Contravariant<T> : IContravariant<T> { } public class Fruit { } public class Apple : Fruit { } public class TheInsAndOuts { public void Covariance() { ICovariant<Fruit> fruit = new Covariant<Fruit>(); ICovariant<Apple> apple = new Covariant<Apple>(); Covariant(fruit); Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile } public void Contravariance() { IContravariant<Fruit> fruit = new Contravariant<Fruit>(); IContravariant<Apple> apple = new Contravariant<Apple>(); Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile Contravariant(apple); } public void Covariant(ICovariant<Fruit> fruit) { } public void Contravariant(IContravariant<Apple> apple) { } }
tldr
ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant
ключевые слова in и out управляют правилами приведения компилятора для интерфейсов и делегатов с общими параметрами:
interface IInvariant<T> { // This interface can not be implicitly cast AT ALL // Used for non-readonly collections IList<T> GetList { get; } // Used when T is used as both argument *and* return type T Method(T argument); }//interface interface ICovariant<out T> { // This interface can be implicitly cast to LESS DERIVED (upcasting) // Used for readonly collections IEnumerable<T> GetList { get; } // Used when T is used as return type T Method(); }//interface interface IContravariant<in T> { // This interface can be implicitly cast to MORE DERIVED (downcasting) // Usually means T is used as argument void Method(T argument); }//interface class Casting { IInvariant<Animal> invariantAnimal; ICovariant<Animal> covariantAnimal; IContravariant<Animal> contravariantAnimal; IInvariant<Fish> invariantFish; ICovariant<Fish> covariantFish; IContravariant<Fish> contravariantFish; public void Go() { // NOT ALLOWED invariants do *not* allow implicit casting: invariantAnimal = invariantFish; invariantFish = invariantAnimal; // NOT ALLOWED // ALLOWED covariants *allow* implicit upcasting: covariantAnimal = covariantFish; // NOT ALLOWED covariants do *not* allow implicit downcasting: covariantFish = covariantAnimal; // NOT ALLOWED contravariants do *not* allow implicit upcasting: contravariantAnimal = contravariantFish; // ALLOWED contravariants *allow* implicit downcasting contravariantFish = contravariantAnimal; }//method }//class // .NET Framework Examples: public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { } public interface IEnumerable<out T> : IEnumerable { } class Delegates { // When T is used as both "in" (argument) and "out" (return value) delegate T Invariant<T>(T argument); // When T is used as "out" (return value) only delegate T Covariant<out T>(); // When T is used as "in" (argument) only delegate void Contravariant<in T>(T argument); // Confusing delegate T CovariantBoth<out T>(T argument); // Confusing delegate T ContravariantBoth<in T>(T argument); // From .NET Framework: public delegate void Action<in T>(T obj); public delegate TResult Func<in T, out TResult>(T arg); }//class
class A {} class B : A {} public void SomeFunction() { var someListOfB = new List<B>(); someListOfB.Add(new B()); someListOfB.Add(new B()); someListOfB.Add(new B()); SomeFunctionThatTakesA(someListOfB); } public void SomeFunctionThatTakesA(IEnumerable<A> input) { // Before C# 4, you couldn't pass in List<B>: // cannot convert from // 'System.Collections.Generic.List<ConsoleApplication1.B>' to // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>' }
в основном всякий раз, когда у вас была функция, которая принимает Перечислимое одного типа, Вы не могли передать Перечислимое производного типа без явного приведения его.
просто, чтобы предупредить вас о ловушке, хотя:
var ListOfB = new List<B>(); if(ListOfB is IEnumerable<A>) { // In C# 4, this branch will // execute... Console.Write("It is A"); } else if (ListOfB is IEnumerable<B>) { // ...but in C# 3 and earlier, // this one will execute instead. Console.Write("It is B"); }
Это ужасный код в любом случае, но он существует, и изменение поведения в C# 4 может привести к тонким и трудным для поиска ошибкам, если вы используете такую конструкцию.
вот простой пример использования иерархии наследования.
учитывая простую иерархию классов:
Giraffe / LifeForm <- Animal <- \ Zebra
в коде:
public abstract class LifeForm { } public abstract class Animal : LifeForm { } public class Giraffe : Animal { } public class Zebra : Animal { }
инвариантность (общий с параметризованным типом, украшенным ни одним
in
, ниout
)по-видимому, такой метод
public static void PrintLifeForms(IList<LifeForm> lifeForms) { foreach (var lifeForm in lifeForms) { Console.WriteLine(lifeForm.GetType().ToString()); } }
... должен принимать гетерогенную коллекцию: (что он и делает)
var myAnimals = new List<LifeForm> { new Giraffe(), new Zebra() }; PrintLifeForms(myAnimals); // Giraffe, Zebra
однако, передавая коллекцию а производного тип не удается!
var myGiraffes = new List<Giraffe> { new Giraffe(), // "Jerry" new Giraffe() // "Melman" }; PrintLifeForms(myGiraffes); // Compile Error!
cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'
почему? Потому что общий параметр
IList<LifeForm>
не является ковариантным -IList<LifeForm>
является инвариантным и принимает только коллекции (которые реализуют IList) , где параметризованный типT
должно бытьLifeForm
.если я злонамеренно изменю метод реализации
PrintLifeForms
(но оставьте ту же сигнатуру метода), причина, по которой компилятор предотвращает передачуList<Giraffe>
становится очевидным:public static void PrintLifeForms(IList<LifeForm> lifeForms) { lifeForms.Add(new Zebra()); }
С
IList
позволяет добавлять или удалять элементы, любой подклассLifeForm
таким образом, можно добавить к параметруlifeForms
, и нарушит тип любой коллекции производных типов, переданных методу. (Здесь вредоносный метод будет пытаться добавитьZebra
доvar myGiraffes
). К счастью, компилятор защищает нас от этой опасности.ковариация (общий с параметризованным типом, украшенным
out
)ковариация широко используется с неизменяемыми коллекциями (т. е. там, где новые элементы не могут быть добавлены или удалены из коллекции)
решение в приведенном выше примере заключается в том, чтобы обеспечить использование ковариантного универсального типа, например
IEnumerable
(определена какIEnumerable<out T>
). Это предотвращает изменение коллекции и, как следствие, любую коллекцию с подтипомLifeForm
теперь можно передать методу:public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms) { foreach (var lifeForm in lifeForms) { Console.WriteLine(lifeForm.GetType().ToString()); } }
PrintLifeForms()
теперь можно назвать сZebras
,Giraffes
и ниIEnumerable<>
любого подклассаLifeForm
контравариантность (общий с параметризованным типом, украшенным
in
)контравариантность часто используется, когда функции передаются в качестве параметров.
вот пример функции, которая принимает
Action<Zebra>
в качестве параметра и вызывает его на известном экземпляре Zebra:public void PerformZebraAction(Action<Zebra> zebraAction) { var zebra = new Zebra(); zebraAction(zebra); }
как и ожидалось, это работает просто отлично:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra")); PerformZebraAction(myAction); // I'm a zebra
интуитивно, это плохо:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe")); PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
однако, это удается
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal")); PerformZebraAction(myAction); // I'm an animal
и даже это тоже удается:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba")); PerformZebraAction(myAction); // I'm an amoeba
почему? Потому что
Action
определяется какAction<in T>
, то есть этоcontravariant
.хотя сначала это может быть неинтуитивным (например, как может
Action<object>
передается как параметр, требующийAction<Zebra>
?), если вы распакуете шаги, вы будете обратите внимание, что вызываемая функция (PerformZebraAction
) сам отвечает за передачу данных (в данном случае aZebra
экземпляр) к функции-данные не поступают из вызывающего кода.из-за инвертированного подхода использования функций более высокого порядка таким образом, к моменту
Action
вызывается, это более производный экземпляр объекта, который вызывается противzebraAction
функция (передается как параметр), которая сама использует менее производный тип.
С MSDN
в следующем примере кода показана поддержка ковариации и контравариации для групп методов
static object GetObject() { return null; } static void SetObject(object obj) { } static string GetString() { return ""; } static void SetString(string str) { } static void Test() { // Covariance. A delegate specifies a return type as object, // but you can assign a method that returns a string. Func<object> del = GetString; // Contravariance. A delegate specifies a parameter type as string, // but you can assign a method that takes an object. Action<string> del2 = SetObject; }
делегат конвертера помогает мне визуализировать обе концепции, работающие вместе:
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
представляет ковариации где метод возвращает a более конкретную типа.
TInput
представляет контравариантность где метод передается a меньше конкретного типа.public class Dog { public string Name { get; set; } } public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } } public static Poodle ConvertDogToPoodle(Dog dog) { return new Poodle() { Name = dog.Name }; } List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } }; List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle)); poodles[0].DoBackflip();
ковариация и контравариация обеспечивают некоторую гибкость в коде.
using System; // ... class Program { static void Main(string[] args) { Console.ForegroundColor = ConsoleColor.Cyan; ReturnPersonDelegate returnPersonDelegate = ReturnPersonMethod; Employee employee = (Employee)returnPersonDelegate(); Console.WriteLine(employee._whatHappened); EmployeeParameterDelegate employeeParameterDelegate = EmployeeParameterMethod; employeeParameterDelegate(new Employee()); Console.ForegroundColor = ConsoleColor.Gray; Console.Write("Press any key to quit . . . "); Console.ReadKey(true); } delegate Person ReturnPersonDelegate(); // Covariance allows derived class as its return value delegate void EmployeeParameterDelegate(Employee employee); // Contravariance allows base class as its parameters static Employee ReturnPersonMethod() { Employee employee = new Employee(); employee._whatHappened = employee + ": Covariance"; return employee; } static void EmployeeParameterMethod(Person person) { person._whatHappened = ": Contravariance"; Console.WriteLine(person + person._whatHappened); } } class Person { public string _whatHappened; } class Employee : Person { }