Ковариация и контравариация пример реального мира


У меня возникли небольшие проблемы с пониманием того, как я буду использовать ковариацию и контравариантность в реальном мире.

до сих пор единственные примеры, которые я видел, были тем же самым старым примером массива.

object[] objectArray = new string[] { "string 1", "string 2" };

было бы неплохо увидеть пример, который позволил бы мне использовать его во время моей разработки, если бы я мог видеть, что он используется в другом месте.

9 120

9 ответов:

допустим, у вас есть классный человек и класс, который происходит от него, учитель. У вас есть некоторые операции, которые принимают IEnumerable<Person> в качестве аргумента. В вашей школе у вас есть метод, который возвращает IEnumerable<Teacher>. Ковариация позволяет непосредственно использовать этот результат для методов, которые принимают IEnumerable<Person>, подставляя более производный тип для менее производного (более общего) типа. Контравариантность, контр-интуитивно, позволяет использовать более общий тип, где указан более производный тип. См. также https://msdn.microsoft.com/en-us/library/dd799517.aspx

public 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) сам отвечает за передачу данных (в данном случае a Zebra экземпляр) к функции-данные не поступают из вызывающего кода.

из-за инвертированного подхода использования функций более высокого порядка таким образом, к моменту 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
{

}