Covariância e Contravariância em C#, Parte Cinco: Contravariância Dupla
Na parte quatro da série, falamos sobre como poderiamos ter um tipo de delegate que aceitaria ter valores covariantes no seu tipo de retorno e contravariantes no tipo recebido por argumento. Por exemplo, nós podemos ter um delegate action contravariante:
delegate void Action< -A > (A a);
e então temos
Action<Animal> action1 = (Animal a)=>{ Console.WriteLine(a.NomeLatin); };
Action<Girafa> action2 = action1;
Porque o invocador do action2 irá sempre passar algo que o action1 possa manipular.
Baseado no que vimos até agora no que diz respeito à variância concluimos que "o que está entrando deve ser contravariante, o que sai deve ser variante". Embora pareça que este seria um uso comum de variância que deveria ser implementada numa futura versão do C#, a realidade é um pouco mais complicada. Há uma situação onde é válido usar um argumento covariante como parâmetro de um delegate.
Suponha que você queira criar uma programação funcional de "ordem maior". Por exemplo, talvez você queira definir uma meta-ação – um delegate que recebe actions e faz alguma coisa com eles:
delegate void Meta<A>(Action<A> action);
Por exemplo:
Meta<Mamifero> meta1 = (Action<Mamifero> action)=>{action(new Girafa());};
// A próxima linha é válida porque Action<Animal> é menor que Action<Mamifero>;
// lembrando que Action é contravariante
meta1(action1);
Então este Meta recebe um Action de Mamiferos, ou o action1 acima, o qual exibe o nome em Latin de qualquer Animal, o que quer dizer que pode ser usado para os Mamifero – e então invoca aquela action usando o new Girafa.
É evidente que o parâmetro de tipo A é usado em uma posição de entrada na definição de Meta<A>, então nós devemos ser capazes de usar contravariancia, certo? Suponha que sim. Isso significa que essa atribuição seria válida:
Meta<Tigre> meta2 = meta1; // deve ser válida se Meta é contraviante no parâmetro
Mas isso significa que isso seria válido:
Action<Tigre> action3 = Tigre=>{ Tigre.Rosnar(); };
meta2(action3);
Seguindo a lógica você verá que no final acabamos chamando (new Girafa()).Rosnar(), o qual claramente viola ambas as regras: do sistema e da natureza. Pode ser um pouco complicado até entender toda a lógica, mas escrevendo o código pode ajudar:
static void Main(string[] args)
{
Action<int> a;
Action<Animal> action1 = metodo;
Action<Girafa> action2 = action1;
Meta<Mamifero> meta1 = metodo2;
meta1(action1);
Meta<Tigre> meta2 = meta1;
Action<Tigre> action3 = metodo3;
meta2(action3);
Meta<Animal> meta3 = meta1;
}
static void metodo3(Tigre Tigre)
{
Tigre.Rosnar();
}
static void metodo2(Action<Mamifero> action)
{
action(new Girafa());
}
static void metodo(Animal a)
{
Console.WriteLine(a.NomeLatin);
}
public delegate void Meta<A>(Action<A> action);
No final do Main, você pode ver que o meta2 é igual ao meta1. O meta1 chama o método 2. O método 2 vai chamar o action passado(no caso o action3 que chama o metodo3) passando um new Girafa() como parâmetro. Neste momento que acontece a inconsistência. Pois estamos chamando o método3 passando uma Girafa, e o método3 iria chamar o método new Girafa()).Rosnar().
Então Meta<A> não pode ser contravariante em A. No entanto ele pode ser covariante:
Meta<Animal> meta3 = meta1; // válido se Meta for covariante
Agora tudo funciona. meta3 recebe um Action sobre Animals e então passa um Girafa para a Action.
Contravariância é complicado. O fato de se inverter o maior/menor relacionamento entre tipos diz que um tipo de parâmetro usado em uma posição de "contravariância dupla" (sendo uma entrada de Action, que é em si uma entrada de Meta) se torna covariante. O segundo desfaz a primeira inversão.
No próximo artigo deixaremos os delegates pra trás e falaremos sobre variância nas interfaces.