Share via


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.