Variância e Contra-variância: uma dívida


Tenho que pagar uma dívida aqui. Falei na volta do PDC que iria falar sobre variância e contra-variância, pois haverá mudanças no C# 4.0. Bem, aqui vai...


Antes de tudo, vou tentar ser simples e lidar com o assunto sem muitos detalhes. Quem quiser saber mais, recomendo a séries de blogs do Eric Lippert aqui. A definição de variância e contra-variância pode ser encontrada aqui.


Variância e contra-variância tem tudo a ver com atribuição entre tipos e sub-tipos. Como diz o comentarista chato da TV, “a regra é clara” – uma atribuição de um objeto de um tipo B para outro do tipo A só é válida se A>= B (A é super-classe ou da mesma classe que B). Assim,



Animal a = new Mamifero(); é válido



Mamifero m = a; é inválido !


Note que isto também tem que ser válido em outras partes da linguagem onde a atribuição está escondida. Por exemplo, no caso da passagem de parâmetros que é, de fato, uma atribuição.


Existem 4 possibilidades básicas numa passagem de parâmetro: cópia do argumento na entrada (parâmetro in), cópia para o argumento na saída (out), cópia do/para o argumento (inout) e cópia da referência (ref). Podemos encarar todas estas cópias como atribuições e uma linguagem que lida com tipos deve evitar passagens inválidas. Assim, tendo definido o método



void M( in Mamifero p1, out Animal p2) {...}


não poderemos usar um Animal para p1 ou um Mamifero para p2 como argumentos na chamada do método M. A regra de atribuição seria quebrada.


Vamos adicionar agora a herança como mais uma dimensão neste jogo. Imagine as seguintes classes:



class A {



      public void M( in Mamifero p1, out Animal p2) {…}



}



class B: A {



     public override void M( in Mamifero p1, out Animal p2) {…}



}


Existe aqui uma oportunidade de sermos mais precisos que, porém, nem todas as linguagens nos permitem. Se as linguagens nos deixassem, poderíamos ser mais precisos e rigorosos na declaração dos parâmetros do método M da classe B em dois sentidos:



1) O parâmetro p1 poderia ter seu tipo amplificado, para Animal, por exemplo (aceita-se a partir de agora um tipo mais amplo);


2) O p2 poderia se tornar mais restritivo, como, por exemplo, Mamifero (limitamos, a partir de agora, a saída para um tipo mais restrito);


Portanto, a definição



class B: A {



     public override void M( in Animal p1, out Mamifero p2) {…}



}


deveria ser plausível numa linguagem orientada a objetos, pois ainda estaria respeitando a movimentação dos argumentos segundo os tipos. Note também que esta restrição/ampliação de tipos dos parâmetros não é uma obrigatoriedade, mas sim uma oportunidade que pode ser usada caso haja sentido, isto é, caso a semântica do método M da classe B permitam esta restrição/ampliação.


Neste contexto, a ampliação/restrição dos tipos dos parâmetro são chamadas variância (as vezes chamada de co-variância) e contra-variância, pois uma acompanha o aumento da restrição que a subclasse já impõe, enquanto a outra vai no sentido oposto.


Existem várias outras dimensões onde esta regra tem importância, como delegação, tipos genéricos e tipos que são conjuntos de outros tipos (ex.: arrays de Mamíferos). Minha intenção aqui não é a de descrever o impacto desta regra em todos os mecanismos lingüísticos, mas sim de dar uma base para que possamos entender as futuras mudanças do C# 4.0 quanto a este tema.


Abraços e me desculpem o blog longo.

Skip to main content