Diferença entre covariância e contra-variância


Respostas:


266

A questão é "qual é a diferença entre covariância e contravariância?"

Covariância e contravariância são propriedades de uma função de mapeamento que associa um membro de um conjunto a outro . Mais especificamente, um mapeamento pode ser covariante ou contravariante em relação a uma relação nesse conjunto.

Considere os dois subconjuntos a seguir do conjunto de todos os tipos de C #. Primeiro:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

E segundo, esse conjunto claramente relacionado:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

Há uma operação de mapeamento do primeiro conjunto para o segundo conjunto. Ou seja, para cada T no primeiro conjunto, o tipo correspondente no segundo conjunto é IEnumerable<T>. Ou, resumidamente, o mapeamento é T → IE<T>. Observe que esta é uma "seta fina".

Comigo até agora?

Agora vamos considerar uma relação . Há um relacionamento de compatibilidade de atribuição entre pares de tipos no primeiro conjunto. Um valor do tipo Tigerpode ser atribuído a uma variável do tipo Animal, portanto, esses tipos são considerados "compatíveis com a atribuição". Vamos escrever "um valor do tipo Xpode ser atribuído a uma variável do tipo Y" de uma forma mais curta: X ⇒ Y. Observe que esta é uma "flecha gorda".

Portanto, em nosso primeiro subconjunto, aqui estão todos os relacionamentos de compatibilidade de atribuição:

Tiger   Tiger
Tiger   Animal
Animal  Animal
Banana  Banana
Banana  Fruit
Fruit   Fruit

No C # 4, que oferece suporte à compatibilidade de designação covariante de certas interfaces, há um relacionamento de compatibilidade de atribuição entre pares de tipos no segundo conjunto:

IE<Tiger>   IE<Tiger>
IE<Tiger>   IE<Animal>
IE<Animal>  IE<Animal>
IE<Banana>  IE<Banana>
IE<Banana>  IE<Fruit>
IE<Fruit>   IE<Fruit>

Observe que o mapeamento T → IE<T> preserva a existência e a direção da compatibilidade da atribuição . Ou seja, se X ⇒ Y, então também é verdade IE<X> ⇒ IE<Y>.

Se tivermos duas coisas em ambos os lados de uma flecha gorda, podemos substituir os dois lados por algo no lado direito de uma flecha fina correspondente.

Um mapeamento que possui essa propriedade em relação a uma relação específica é chamado de "mapeamento covariante". Isso deve fazer sentido: uma sequência de Tigres pode ser usada onde uma sequência de Animais é necessária, mas o contrário não é verdadeiro. Uma sequência de animais não pode necessariamente ser usada quando uma sequência de Tigres é necessária.

Isso é covariância. Agora considere este subconjunto do conjunto de todos os tipos:

{ IComparable<Tiger>, 
  IComparable<Animal>, 
  IComparable<Fruit>, 
  IComparable<Banana> }

agora temos o mapeamento do primeiro para o terceiro T → IC<T>.

Em C # 4:

IC<Tiger>   IC<Tiger>
IC<Animal>  IC<Tiger>     Backwards!
IC<Animal>  IC<Animal>
IC<Banana>  IC<Banana>
IC<Fruit>   IC<Banana>     Backwards!
IC<Fruit>   IC<Fruit>

Ou seja, o mapeamento T → IC<T>tem preservado a existência, mas inverteu a direção de compatibilidade atribuição. Isso é se X ⇒ Y, então IC<X> ⇐ IC<Y>.

Um mapeamento que preserva, mas reverte uma relação, é chamado de mapeamento contravariante .

Novamente, isso deve estar claramente correto. Um dispositivo que pode comparar dois animais também pode comparar dois tigres, mas um dispositivo que pode comparar dois tigres não pode necessariamente comparar dois animais.

Portanto, essa é a diferença entre covariância e contravariância no C # 4. A covariância preserva a direção da atribuição. A contravariância reverte isso.


4
Para alguém como eu, seria melhor adicionar exemplos mostrando o que NÃO é covariante e o que NÃO é contravariante e o que NÃO é ambos.
bjan

2
@ Bargitta: É muito parecido. A diferença é que o C # usa a variação definida do site e o Java usa a variação do site de chamada . Portanto, a maneira como as coisas variam é a mesma, mas onde o desenvolvedor diz "Eu preciso que isso seja variante" é diferente. Aliás, o recurso nos dois idiomas foi em parte projetado pela mesma pessoa!
Eric Lippert

2
@AshishNegi: Leia a seta como "pode ​​ser usada como". "Uma coisa que pode comparar animais pode ser usada como uma coisa que pode comparar tigres". Faz sentido agora?
Eric Lippert

1
@AshishNegi: Não, isso não está certo. IEnumerable é covariante porque T aparece apenas nos retornos dos métodos de IEnumerable. E IComparable é contravariante porque T aparece apenas como parâmetros formais dos métodos de IComparable .
Eric Lippert

2
@AshishNegi: Você quer pensar nas razões lógicas subjacentes a esses relacionamentos. Por que podemos converter IEnumerable<Tiger>para IEnumerable<Animal>com segurança? Porque não há como inserir uma girafa IEnumerable<Animal>. Por que podemos converter um IComparable<Animal>para IComparable<Tiger>? Porque não há como tirar uma girafa de um IComparable<Animal>. Faz sentido?
Eric Lippert

111

Provavelmente é mais fácil dar exemplos - é certamente assim que me lembro deles.

Covariância

Exemplos: canónicos IEnumerable<out T>,Func<out T>

Você pode converter de IEnumerable<string>para IEnumerable<object>ou Func<string>para Func<object>. Os valores só vem a partir desses objetos.

Funciona porque, se você estiver tirando apenas valores da API e retornar algo específico (como string), poderá tratar esse valor retornado como um tipo mais geral (como object).

Contravariância

Exemplos: canónicos IComparer<in T>,Action<in T>

Você pode converter de IComparer<object>para IComparer<string>ou Action<object>para Action<string>; valores só vão para esses objetos.

Desta vez, funciona porque, se a API está esperando algo geral (como object), você pode fornecer algo mais específico (como string).

De forma geral

Se você possui uma interface, IFoo<T>ela pode ser covariável T(por exemplo, declarar como IFoo<out T>se Tfosse usada apenas em uma posição de saída (por exemplo, um tipo de retorno) dentro da interface. Pode ser contravariante em T(por exemplo IFoo<in T>) se Tfor usada apenas em uma posição de entrada ( por exemplo, um tipo de parâmetro).

Fica potencialmente confuso porque "posição de saída" não é tão simples quanto parece - um parâmetro do tipo Action<T>ainda está sendo usado apenas Tem uma posição de saída - a contravariância de invertê- Action<T>lo, se você entende o que quero dizer. É uma "saída" em que os valores podem passar da implementação do método para o código do chamador, assim como um valor de retorno. Normalmente esse tipo de coisa não aparece, felizmente :)


1
Para alguém como eu, seria melhor adicionar exemplos mostrando o que NÃO é covariante e o que NÃO é contravariante e o que NÃO é ambos.
bjan

1
@ Jon Skeet Bom exemplo, eu só não entendo "um parâmetro do tipo Action<T>ainda está usando apenas Tem uma posição de saída" . Action<T>tipo de retorno é nulo, como ele pode ser usado Tcomo saída? Ou é isso que significa, porque não retorna nada que você possa ver que nunca pode violar a regra?
Alexander Derck

2
Para meu futuro eu, que está voltando para este excelente resposta de novo para reaprender a diferença, esta é a linha que deseja: "[Covariance] funciona porque se você está tendo apenas valores fora do API, e que vai retornar algo específico (como string), você pode tratar esse valor retornado como um tipo mais geral (como objeto). "
Matt Klein

A parte mais confusa de tudo isso é que, por covariância ou contravariância, se você ignora a direção (entrada ou saída), obtém a conversão Mais Específica para Mais Genérica! Quero dizer: "você pode tratar esse valor retornado como um tipo mais geral (como objeto)" para covariância e: "A API está esperando algo geral (como objeto), você pode fornecer algo mais específico (como string)" para contravariância . Para mim, esses sons são iguais!
XMight 06/03

@AlexanderDerck: Não sei por que não lhe respondi antes; Concordo que não está claro e tentarei esclarecer isso.
Jon Skeet

16

Espero que meu post ajude a obter uma visão independente do idioma sobre o tópico.

Para nossos treinamentos internos, trabalhei com o maravilhoso livro "Smalltalk, Objects and Design (Chamond Liu)" e reformulei os exemplos a seguir.

O que significa "consistência"? A idéia é projetar hierarquias de tipo com segurança de tipo com tipos altamente substituíveis. A chave para obter essa consistência é a conformidade baseada em subtipo, se você trabalhar em um idioma digitado estaticamente. (Discutiremos o Princípio de Substituição de Liskov (LSP) em alto nível aqui.)

Exemplos práticos (pseudocódigo / inválido em C #):

  • Covariância: Vamos supor que os pássaros que põem ovos "consistentemente" com a digitação estática: se o tipo pássaro põe um ovo, o subtipo de pássaro não colocaria um subtipo de ovo? Por exemplo, o tipo Duck estabelece um DuckEgg, então a consistência é fornecida. Por que isso é consistente? Porque em tal expressão: Egg anEgg = aBird.Lay();a referência aBird pode ser legalmente substituída por uma instância Bird ou Duck. Dizemos que o tipo de retorno é covariante para o tipo em que Lay () está definido. A substituição de um subtipo pode retornar um tipo mais especializado. => "Eles entregam mais."

  • Contravariância: Vamos supor aos pianos que os pianistas podem tocar "consistentemente" com a digitação estática: se um pianista toca piano, ela seria capaz de tocar um GrandPiano? Não preferiria um virtuoso tocar um GrandPiano? (Esteja avisado; há uma torção!) Isso é inconsistente! Porque nessa expressão: aPiano.Play(aPianist);aPiano não poderia ser legalmente substituído por um Piano ou por uma instância GrandPiano! Um GrandPiano só pode ser jogado por um Virtuoso, os pianistas são muito gerais! Os GrandPianos devem ser reproduzidos por tipos mais gerais; a reprodução é consistente. Dizemos que o tipo de parâmetro é contrário ao tipo em que Play () está definido. A substituição de um subtipo pode aceitar um tipo mais generalizado. => "Eles exigem menos."

Voltar ao C #:
como o C # é basicamente uma linguagem de tipo estaticamente, os "locais" da interface de um tipo que devem ser co-ou contravariantes (por exemplo, parâmetros e tipos de retorno), devem ser marcados explicitamente para garantir um uso / desenvolvimento consistente desse tipo , para fazer o LSP funcionar bem. Em linguagens tipadas dinamicamente, a consistência do LSP normalmente não é um problema; em outras palavras, você pode se livrar completamente da "marcação" co- e contravariante nas interfaces e delegados .Net, se você apenas usar o tipo dinâmico nos seus tipos. - Mas essa não é a melhor solução em C # (você não deve usar dinâmico em interfaces públicas).

Voltar à teoria:
A conformidade descrita (tipos de retorno covariante / tipos de parâmetro contravariante) é o ideal teórico (suportado pelas línguas Emerald e POOL-1). Algumas linguagens oop (por exemplo, Eiffel) decidiram aplicar outro tipo de consistência, esp. também tipos de parâmetros covariantes, porque descreve melhor a realidade do que o ideal teórico. Nas linguagens de tipo estaticamente, a consistência desejada geralmente deve ser alcançada pela aplicação de padrões de design como "expedição dupla" e "visitante". Outros idiomas fornecem os chamados métodos de “despacho múltiplo” ou multi (isso basicamente seleciona sobrecargas de funções em tempo de execução , por exemplo, com CLOS) ou obtém o efeito desejado usando a digitação dinâmica.


Você diz que a substituição de um subtipo pode retornar um tipo mais especializado . Mas isso é completamente falso. Se Birddefine public abstract BirdEgg Lay();, então Duck : Bird DEVE implementar public override BirdEgg Lay(){}Portanto, sua afirmação de que BirdEgg anEgg = aBird.Lay();há qualquer tipo de variação é simplesmente falsa. Sendo a premissa do ponto da explicação, todo o ponto agora se foi. Em vez disso, você diria que a covariância existe na implementação em que um DuckEgg é implicitamente convertido no tipo de saída / retorno BirdEgg? De qualquer maneira, por favor, esclareça minha confusão.
Suamere

1
Para resumir: você está certo! Desculpe pela confusão. DuckEgg Lay()não é uma substituição válida para Egg Lay() em C # , e esse é o ponto crucial. O C # não suporta tipos de retorno covariantes, mas Java, assim como o C ++, suportam. Eu descrevi o ideal teórico usando uma sintaxe semelhante a C #. Em C #, você precisa permitir que Bird e Duck implementem uma interface comum, na qual Lay é definido para ter um retorno covariante (isto é, a especificação fora de especificação), para que as questões se encaixem!
Nico

1
Como análogo ao comentário de Matt-Klein na resposta de Jon-Skeet, "para o meu eu futuro": a melhor coisa para mim aqui é "Eles entregam mais" (específico) e "Eles exigem menos" (específico). "Exigir menos e entregar mais" é um excelente mnemônico! É análogo a um trabalho em que espero exigir instruções menos específicas (solicitações gerais) e ainda assim oferecer algo mais específico (um produto de trabalho real). De qualquer maneira, a ordem dos subtipos (LSP) é ininterrupta.
karfus 31/07/19

@karfus: Obrigado, mas como eu me lembro, parafraseiei a idéia "Exigir menos e entregar mais" de outra fonte. Pode ser que foi o livro de Liu a que me refiro acima ... ou mesmo uma palestra sobre .NET Rock. Btw. em Java, as pessoas reduziram o mnemônico para "PECS", que se relaciona diretamente à maneira sintática de declarar variações, o PECS é para "Produtor extends, Consumidor super".
Nico

5

O delegado do conversor me ajuda a entender a diferença.

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

0

As variações Co e Contra são coisas bastante lógicas. O sistema de tipos de linguagem nos obriga a apoiar a lógica da vida real. É fácil entender por exemplo.

Covariância

Por exemplo, você quer comprar uma flor e tem duas lojas de flores em sua cidade: loja de rosas e loja de margaridas.

Se você perguntar a alguém "onde fica a loja de flores?" e alguém lhe diz onde fica a loja de rosas, tudo bem? Sim, porque rosa é uma flor, se você quiser comprar uma flor, pode comprar uma rosa. O mesmo se aplica se alguém lhe responder com o endereço da loja de margaridas.

Este é um exemplo de covariância : você tem permissão para converter A<C>para A<B>, onde Cestá uma subclasse de B, se Aproduz valores genéricos (retorna como resultado da função). A covariância é sobre produtores, é por isso que o C # usa a palavra out- chave para covariância.

Tipos:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

A pergunta é "onde fica a loja de flores?", A resposta é "loja de rosas lá":

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

Contravariância

Por exemplo, você quer presentear uma flor com sua namorada e sua namorada gosta de flores. Você pode considerá-la como uma pessoa que ama rosas ou como uma pessoa que ama margaridas? Sim, porque se ela ama alguma flor, ela amaria tanto a rosa quanto a margarida.

Este é um exemplo da contravariância : você pode converter A<B>para A<C>, onde Cestá a subclasse de B, se Aconsumir valor genérico. Contravariância é sobre consumidores, é por isso que o C # usa a palavra in- chave para contravariância.

Tipos:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

Você está considerando sua namorada que ama uma flor como alguém que ama rosas e está dando uma rosa para ela:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Ligações

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.