Exemplo de covariância e contravariância no mundo real


162

Estou com um pouco de dificuldade para entender como usaria covariância e contravariância no mundo real.

Até agora, os únicos exemplos que vi foram o mesmo exemplo antigo de matriz.

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

Seria bom ver um exemplo que me permitisse usá-lo durante o meu desenvolvimento se eu pudesse vê-lo sendo usado em outro lugar.


1
Eu exploro a covariância nesta resposta à (minha) pergunta: tipos de covariância: por exemplo . Eu acho que você achará interessante, e espero instrutivo.
Cristian Diaconescu

Respostas:


109

Digamos que você tenha uma Pessoa da classe e uma classe que dela deriva, Professor. Você tem algumas operações que aceitam um IEnumerable<Person>como argumento. Na sua turma da escola, você tem um método que retorna um IEnumerable<Teacher>. A covariância permite que você use diretamente esse resultado para os métodos que usam um IEnumerable<Person>, substituindo um tipo mais derivado por um tipo menos derivado (mais genérico). Contravariância, contra-intuitivamente, permite usar um tipo mais genérico, onde um tipo mais derivado é especificado.

Consulte também Covariância e Contravariância em genéricos no MSDN .

Classes :

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);
    }
}

Uso :

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<in 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());

14
@FilipBartuzi - se, como eu quando escrevi esta resposta, você estava empregado em uma universidade que é um exemplo do mundo real.
precisa saber é o seguinte

5
Como isso pode ser marcado como a resposta quando ela não responde à pergunta e não fornece nenhum exemplo de uso de co / contra variância em c #?
barakcaf

@barakcaf adicionou um exemplo de contravariância. não sei por que você não estava vendo o exemplo de covariância - talvez você precisasse rolar o código para baixo -, mas adicionei alguns comentários a esse respeito.
tvanfosson

@tvanfosson o código usa co / contra, e não mostra como declará-lo. O exemplo não mostra o uso de in / out na declaração genérica enquanto a outra resposta o faz.
barakcaf

Então, se eu entendi direito, covariância é o que permite o princípio de substituição de Liskov em C #, certo?
Miguel Veloso

136
// 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();

Para completar…

// 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??

138
Eu gosto deste exemplo realista. Eu estava escrevendo um código para devorar burros na semana passada e fiquei muito feliz por termos covariância agora. :-)
Eric Lippert

4
Este comentário acima com @javadba dizendo ao EricLippert o que é covariância e contravariância é um exemplo coviante realista de eu dizendo à minha avó como chupar ovos! : p
iAteABug_And_iLiked_it

1
A pergunta não perguntou o que contravariância e covariância podem fazer ; perguntou por que você precisaria usá-la . Seu exemplo está longe de ser prático porque também não exige. Posso criar um QuadrupedGobbler e tratá-lo como ele próprio (atribuí-lo ao IGobbler <Quadruped>) e ainda assim devorar os burros (posso passar um burro para o método Gobble que requer um quadrúpede). Nenhuma contravariância é necessária. Isso é legal que pode tratar uma QuadrupedGobbler como um DonkeyGobbler, mas por que precisamos, neste caso, se um QuadrupedGobbler já pode devorar Burros?
wired_in 27/02

1
@wired_in Porque quando você se importa apenas com burros, ser mais geral pode atrapalhar. Por exemplo, se você tem uma fazenda que fornece burros a serem devorados, você pode expressar isso como void feed(IGobbler<Donkey> dg). Se você usasse um IGobbler <Quadruped> como parâmetro, não poderia passar um dragão que só come burros.
Marcelo Cantos

1
Cheguei atrasado para a festa, mas esse é o melhor exemplo escrito que eu já vi na SO. Faz todo sentido, sendo ridículo. Eu vou ter que o meu jogo com respostas ...
Jesse Williams

120

Aqui está o que eu montei para me ajudar a entender a diferença

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

10
Esta é a melhor coisa que vi até agora que é clara e concisa. Grande exemplo!
Rob L

6
Como a fruta pode ser reduzida para maçã (no Contravarianceexemplo) quando Fruité o pai ou mãe Apple?
quer

@TobiasMarschall isso significa que você precisa estudar mais sobre "polimorfismo"
snr

56

As palavras-chave in e out controlam as regras de conversão do compilador para interfaces e delegados com parâmetros genéricos:

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

Supondo que o peixe é um subtipo de animal. Ótima resposta por sinal.
Rajan Prasad #

48

Aqui está um exemplo simples usando uma hierarquia de herança.

Dada a hierarquia de classes simples:

insira a descrição da imagem aqui

E no código:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Invariância (ou seja, parâmetros de tipo genérico * não * decorados com inou outpalavras-chave)

Aparentemente, um método como este

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... deve aceitar uma coleção heterogênea: (o que faz)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

No entanto, a transmissão de uma coleção de um tipo mais derivado falha!

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>'

Por quê? Como o parâmetro genérico IList<LifeForm>não é covariante - IList<T>é invariável, IList<LifeForm>apenas aceita coleções (que implementam IList) onde o tipo parametrizado Tdeve estar LifeForm.

Se a implementação do método PrintLifeFormsera maliciosa (mas tem a mesma assinatura de método), a razão pela qual o compilador impede a passagem List<Giraffe>se torna óbvia:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Como IListpermite adicionar ou remover elementos, qualquer subclasse de LifeFormpoderia, portanto, ser adicionada ao parâmetro lifeFormse violaria o tipo de qualquer coleção de tipos derivados passada para o método. (Aqui, o método malicioso poderia tentar adicionar um Zebraa var myGiraffes). Felizmente, o compilador nos protege deste perigo.

Covariância (genérico com tipo parametrizado decorado com out)

A covariância é amplamente usada com coleções imutáveis ​​(ou seja, onde novos elementos não podem ser adicionados ou removidos de uma coleção)

A solução para o exemplo acima é garantir que um tipo de coleção genérica covariante seja usado, por exemplo IEnumerable(definido como IEnumerable<out T>). IEnumerablenão possui métodos para alterar a coleção e, como resultado da outcovariância, qualquer coleção com o subtipo de LifeFormagora pode ser passada para o método:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormsagora pode ser chamado com Zebras, Giraffese qualquer IEnumerable<>de qualquer subclasse deLifeForm

Contravariância (genérico com tipo parametrizado decorado com in)

Contravariância é freqüentemente usada quando funções são passadas como parâmetros.

Aqui está um exemplo de uma função, que recebe um Action<Zebra>como parâmetro e a invoca em uma instância conhecida de uma Zebra:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Como esperado, isso funciona muito bem:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Intuitivamente, isso falhará:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

No entanto, isso consegue

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

e até isso também tem sucesso:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

Por quê? Porque Actioné definido como Action<in T>, isto é contravariant, significa que Action<Zebra> myAction, para , isso myActionpode ser "no máximo" a Action<Zebra>, mas Zebratambém são aceitáveis superclasses de menos derivadas de .

Embora isso possa não ser intuitivo a princípio (por exemplo, como pode Action<object>ser passado como um parâmetro exigindo Action<Zebra>?), Se você descompactar as etapas, notará que a função chamada ( PerformZebraAction) é responsável por transmitir dados (neste caso, uma Zebrainstância ) para a função - os dados não provêm do código de chamada.

Devido à abordagem invertida do uso de funções de ordem superior dessa maneira, no momento em que Actioné invocada, é a Zebrainstância mais derivada invocada contra a zebraActionfunção (passada como parâmetro), embora a própria função use um tipo menos derivado.


7
Esta é uma ótima explicação para as diferentes opções de variância, uma vez que fala através do exemplo e também esclarece por que os restringe compilador ou autorizações sem as palavras-chave / out
Vikhram

Onde a inpalavra - chave é usada para a contravariância ?
javadba

@javadba no exemplo acima Action<in T>e Func<in T, out TResult>são contrários ao tipo de entrada. (Meus exemplos usam os tipos existentes invariáveis ​​(Lista), covariantes (IEnumerable) e contravariantes (Ação, Func))
StuartLC

Ok, eu não faço C#isso não saberia disso.
javadba

É bastante semelhante em Scala, apenas sintaxe diferente - [+ T] seria covariante em T, [-T] seria contravariante em T, Scala também pode impor a restrição 'entre' e a subclasse promíscua 'Nada', que C # não tem.
StuartLC

32
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>'
}

Basicamente, sempre que você tinha uma função que utiliza um Enumerable de um tipo, não era possível transmitir um Enumerable de um tipo derivado sem convertê-lo explicitamente.

Apenas para avisar sobre uma armadilha:

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");
}

De qualquer forma, esse código é horrível, mas ele existe e a mudança de comportamento no C # 4 pode apresentar erros sutis e difíceis de encontrar se você usar uma construção como esta.


Portanto, isso afeta as coleções mais do que tudo, porque no c # 3 você pode passar um tipo mais derivado para um método de um tipo menos derivado.
Lâmina

3
Sim, a grande mudança é que o IEnumerable agora suporta isso, enquanto isso não acontecia antes.
Michael Stum

4

Do MSDN

O exemplo de código a seguir mostra suporte a covariância e contravariância para grupos de métodos

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;
}

4

Contravariância

No mundo real, você sempre pode usar um abrigo para animais em vez de um abrigo para coelhos, porque toda vez que um abrigo para animais hospeda um coelho, ele é um animal. No entanto, se você usar um abrigo de coelho em vez de um abrigo de animais, sua equipe poderá ser comida por um tigre.

No código, isso significa que se você tiver um IShelter<Animal> animals, você pode simplesmente escrever IShelter<Rabbit> rabbits = animals , se você prometer e uso Tno IShelter<T>apenas como parâmetros do método assim:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

e substituir um item com um mais genérico, ou seja, reduzir a variância ou introduzir contra variância.

Covariância

No mundo real, você sempre pode usar um fornecedor de coelhos em vez de um fornecedor de animais, porque toda vez que um fornecedor de coelhos lhe dá um coelho, ele é um animal. No entanto, se você usar um fornecedor de animais em vez de um fornecedor de coelho, poderá ser comido por um tigre.

No código, isso significa que, se você tiver um, ISupply<Rabbit> rabbitspode simplesmente escrever ISupply<Animal> animals = rabbits se prometer e usar To ISupply<T>método only only como retorno dos seguintes valores:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

e substituir um item com um mais derivada um, ou seja, aumentar a variância ou introduzir co variância.

Em suma, esta é apenas uma promessa verificável em tempo de compilação de você de que você trataria um tipo genérico de uma certa maneira para manter a segurança do tipo e não fazer com que ninguém comesse.

Você pode querer ler isso para entender melhor isso.


você pode ser comido por um tigre que valeu a pena
votar

Seu comentário contravarianceé interessante. Estou lendo como indicação de um requisito operacional : que o tipo mais geral deve suportar os casos de uso de todos os tipos derivados. Portanto, nesse caso, o abrigo de animais deve ser capaz de oferecer suporte a todos os tipos de animais. Nesse caso, adicionar uma nova subclasse pode quebrar a superclasse! Ou seja, se adicionarmos um subtipo Tyrannosaurus Rex , ele poderá destruir nosso abrigo de animais existente .
javadba

(Contínuo). Isso difere bastante da covariância que é claramente descrita estruturalmente : todos os subtipos mais específicos suportam as operações definidas no supertipo - mas não necessariamente da mesma maneira.
javadba

3

O delegado do conversor me ajuda a visualizar os dois conceitos trabalhando juntos:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputrepresenta covariância em que um método retorna um tipo mais específico .

TInputrepresenta contravariância em que um método é passado para um tipo menos específico .

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();
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.