O que é covariância e contravariância na programação?

Covariância e contravariância são termos que descrevem como uma linguagem de programação lida com subtipos. A variação de um tipo determina se seus subtipos podem ser usados de forma intercambiável com ele.
Variância é um conceito que pode parecer opaco até que um exemplo concreto seja fornecido. Vamos considerar um tipo de base Animal com um subtipo de Cachorro.
interface Animal & # 123; caminhada de função pública & # 40; & # 41 ;: void; & # 125; interface Cão estende Animal & # 123; latido de função pública & # 40; & # 41 ;: void; & # 125;
Todos os “ Animais ” pode andar, mas apenas “ Cachorros ” pode latir. Agora, vamos considerar o que acontece quando essa hierarquia de objetos é usada em nosso aplicativo.
Conectando interfaces
Como todo animal pode andar, podemos criar uma interface genérica que exercita qualquer animal.
interface AnimalController & # 123; exercício de função pública & # 40; Animal $ Animal & # 41 ;: void; & # 125;
O AnimalController tem um método exercise () que typehints a interface Animal.
interface DogRepository & # 123; publicfunction getById & # 40; int $ id & # 41 ;: Dog; & # 125;
Agora temos um DogRepository com um método que certamente retorna um Dog.
O que acontece se tentarmos usar este valor com o AnimalController?
$ AnimalController- > exercício & # 40; $ DogRepository- > getById & # 40; 1 & # 41; & # 41 ;;
Isso é permitido em linguagens em que parâmetros covariantes são suportados. AnimalController deve receber um Animal. O que estamos passando é, na verdade, um Cachorro, mas ainda assim satisfaz o contrato do Animal.
Este tipo de relacionamento é particularmente importante quando você está estendendo classes. Podemos querer um AnimalRepository genérico que recupere qualquer animal sem os detalhes de sua espécie.
interface AnimalRepository & # 123; publicfunction getById & # 40; int $ id & # 41 ;: Animal; & # 125; interface DogRepository estende AnimalRepository & # 123; publicfunction getById & # 40; int $ id & # 41 ;: Dog; & # 125;
DogRepository modifica o contrato do AnimalRepository — pois os chamadores obterão um cão em vez de um animal — mas não o altera fundamentalmente. Ele está apenas sendo mais específico sobre o tipo de retorno. Um cachorro ainda é um animal. Os tipos são covariantes, portanto, a definição de DogRepository é aceitável.
Olhando para a contravariância
Vamos agora considerar o exemplo inverso. Pode ser desejável ter um DogController, que altera a maneira como “ Cães ” são exercidos. Logicamente, isso ainda poderia estender a interface AnimalController. No entanto, na prática, a maioria dos idiomas não permite que você substitua exercício () da maneira necessária.
interface AnimalController & # 123; exercício de função pública & # 40; Animal $ Animal & # 41 ;: void; & # 125; interface DogController estende AnimalController & # 123; exercício de função pública & # 40; Dog $ Dog & # 41 ;: void; & # 125;
Neste exemplo, DogController especificou que o exercício () aceita apenas um Cachorro. Isso está em conflito com a definição do upstream no AnimalController, que permite qualquer “ Animal ” para ser passado. Para cumprir o contrato, DogController deve, portanto, aceitar também qualquer Animal.
À primeira vista, isso pode parecer confuso e inútil. O raciocínio por trás dessa restrição torna-se mais claro quando você &’ re datilografa o AnimalController:
função exercícioAnimal & # 40; AnimalController $ AnimalController, AnimalRepository $ AnimalRepository, int $ id & # 41 ;: void & # 123; $ AnimalController- > exercício & # 40; $ AnimalRepository- > getById & # 40; $ id & # 41; & # 41 ;; & # 125;
O problema é que AnimalController pode ser um AnimalController ou um DogController — nosso método não é para saber qual implementação de interface ele &’ está usando. Isso se deve às mesmas regras de covariância que foram úteis anteriormente.
Como AnimalController pode ser um DogController, agora há um bug sério de tempo de execução aguardando descoberta. AnimalRepository sempre retorna um Animal, portanto, se $ AnimalController for um DogController, o aplicativo irá travar. O tipo Animal é muito vago para passar para o método DogControllerexercise ().
É importante notar que as linguagens que suportam a sobrecarga de método aceitariam DogController. A sobrecarga permite definir vários métodos com o mesmo nome, desde que tenham assinaturas diferentes (eles têm parâmetros e / ou tipos de retorno diferentes). DogController teria um método exercício () extra que aceitava apenas “ Cães. ” No entanto, também seria necessário implementar a assinatura upstream aceitando qualquer “ Animal. ”
Lidando com problemas de variância
Todos os itens acima podem ser resumidos dizendo que os tipos de retorno de função podem ser covariantes, enquanto os tipos de argumento devem ser contravariantes. Isso significa que uma função pode retornar um tipo mais específico do que o definido pela interface. Ele também pode aceitar um tipo mais abstrato como argumento (embora as linguagens de programação mais populares não implementem isso).
Você costuma encontrar problemas de variação ao trabalhar com genéricos e coleções. Nesses cenários, você geralmente deseja uma AnimalCollection e uma DogCollection. DogCollection deve estender AnimalCollection?
Esta é a aparência dessas interfaces:
interface AnimalCollection & # 123; publicfunction add & # 40; Animal $ a & # 41 ;: void; publicfunction getById & # 40; int $ id & # 41 ;: Animal; & # 125; interface DogCollection estende AnimalCollection & # 123; publicfunction add & # 40; Dog $ d & # 41 ;: void; publicfunction getById & # 40; int $ id & # 41 ;: Dog; & # 125;
Olhando primeiro para getById (), Dog é um subtipo de Animal. Os tipos são covariantes e os tipos de retorno covariantes são permitidos. Isso é aceitável. Observamos o problema de variação novamente com add () embora — DogCollection deve permitir que qualquer Animal seja adicionado a fim de satisfazer o contrato AnimalCollection.
Normalmente, esse problema é mais bem resolvido tornando as coleções imutáveis. Permitir apenas que novos itens sejam adicionados ao construtor da coleção. Você pode então eliminar o método add () completamente, tornando AnimalCollection um candidato válido para DogCollection herdar.
Outras formas de variação
Além de covariância e contravariância, você também pode encontrar os seguintes termos:
- Bivariante: um sistema de tipos é bivariada se a covariância e a contravariância se aplicam simultaneamente a uma relação de tipo. A bivariância era usada pelo TypeScript para seus parâmetros antes do TypeScript 2.6
- Variante: os tipos são variantes se a covariância ou a contravariância se aplicarem.
- Invariante: qualquer tipo que não seja variante.
Você normalmente trabalhará com tipos covariantes ou contravariantes. Em termos de herança de classe, um tipo B é covariante com um tipo A se estender A. Um tipo B é contravariante com um tipo A se for o ancestral de B.
Conclusão
Variância é um conceito que explica as limitações dos sistemas de tipos. Normalmente, você só precisa se lembrar que a covariância é aceita em tipos de retorno, enquanto a contravariância é usada para parâmetros.
As regras de variação surgem do princípio de substituição de Liskov. Isso afirma que você deve ser capaz de substituir as instâncias de uma classe por instâncias de suas subclasses sem alterar nenhuma das propriedades do sistema mais amplo. Isso significa que se o Tipo B estende o Tipo A, as instâncias de A podem ser substituídas por instâncias de B.
Usar nosso exemplo acima significa que devemos ser capazes de substituir Animal por Dog, ou AnimalController por DogController. Aqui, vemos novamente porque DogController não pode substituir exercício () para aceitar apenas cães — nós &’ não poderíamos mais substituir AnimalController por DogController, já que os consumidores que atualmente passam por um Animal agora precisam fornecer um cão. A covariância e a contravariância impõem o LSP e garantem padrões consistentes de comportamento.
Nenhum comentário