Estou tendo problemas para entender a diferença entre covariância e contravariância.
fonte
Estou tendo problemas para entender a diferença entre covariância e contravariância.
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 Tiger
pode 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 X
pode 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.
IEnumerable<Tiger>
paraIEnumerable<Animal>
com segurança? Porque não há como inserir uma girafaIEnumerable<Animal>
. Por que podemos converter umIComparable<Animal>
paraIComparable<Tiger>
? Porque não há como tirar uma girafa de umIComparable<Animal>
. Faz sentido?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>
paraIEnumerable<object>
ouFunc<string>
paraFunc<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 (comoobject
).Contravariância
Exemplos: canónicos
IComparer<in T>
,Action<in T>
Você pode converter de
IComparer<object>
paraIComparer<string>
ouAction<object>
paraAction<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 (comostring
).De forma geral
Se você possui uma interface,
IFoo<T>
ela pode ser covariávelT
(por exemplo, declarar comoIFoo<out T>
seT
fosse usada apenas em uma posição de saída (por exemplo, um tipo de retorno) dentro da interface. Pode ser contravariante emT
(por exemploIFoo<in T>
) seT
for 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 apenasT
em 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 :)fonte
Action<T>
ainda está usando apenasT
em uma posição de saída" .Action<T>
tipo de retorno é nulo, como ele pode ser usadoT
como saída? Ou é isso que significa, porque não retorna nada que você possa ver que nunca pode violar a regra?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.
fonte
Bird
definepublic abstract BirdEgg Lay();
, entãoDuck : Bird
DEVE implementarpublic override BirdEgg Lay(){}
Portanto, sua afirmação de queBirdEgg 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.DuckEgg Lay()
não é uma substituição válida paraEgg 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!extends
, Consumidorsuper
".O delegado do conversor me ajuda a entender a diferença.
TOutput
representa covariância em que um método retorna um tipo mais específico .TInput
representa contravariância em que um método é passado para um tipo menos específico .fonte
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>
paraA<B>
, ondeC
está uma subclasse deB
, seA
produz valores genéricos (retorna como resultado da função). A covariância é sobre produtores, é por isso que o C # usa a palavraout
- chave para covariância.Tipos:
A pergunta é "onde fica a loja de flores?", A resposta é "loja de rosas lá":
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>
paraA<C>
, ondeC
está a subclasse deB
, seA
consumir valor genérico. Contravariância é sobre consumidores, é por isso que o C # usa a palavrain
- chave para contravariância.Tipos:
Você está considerando sua namorada que ama uma flor como alguém que ama rosas e está dando uma rosa para ela:
Ligações
fonte