Covariância e Contravariância em C#, Parte Onze: Ao infinito, mas não além
Como discutido ao longo desse espaço, estavamos considerando adicionar Covariância e Contravariância em tipos de parâmetros de delegates e interfaces para uma futura versão do C#.
Variância é extremamente útil em muitos casos, mas expõe um problema irritante em certos casos raros e bizarros. Este é apenas um exemplo:
Considere uma interface "normal" contravariante em seu único parâmetro de tipo genérico, e uma interface "louca" invariante que herda a interface normal em uma forma estranha:
public interface IN<in U> {}
public interface IC<X> : IN<IN<IC<IC<X>>>> {}
Isto é um pouco estranho, mas com certeza válido.
Antes de continuarmos, vamos entender porque isto é válido. A maioria das pessoas quando ve algo como isso imediatamente diz "mas uma interface não pode herdar de si mesma, isso é inválido, é uma cadeia circular na herança!"
Primeiro, não, isso não é correto. Em nenhum lugar a especificação do C# faz este tipo de herança inválido, e de fato, uma forma mais simples disso deve ser válida. Você deve ser capaz de dizer:
interface INumber<X> : IComparable<INumber<X>> { ... }
ou seja, você deve ser capaz de expressar que uma das garantias do contrato de INumber<X> é que você possa sempre comparar um número com outro. Portanto, deve ser válido usar um nome de tipo no argumento de tipo do tipo generic do pai.
No entanto, nem tudo é perfeito. Este tipo particular bruto de herança que dei como exemplo é de fato inválido no CLR, mesmo que não seja inválido no C#. Isso significa que é possível ter um tipo de interface compilada em C# que não pode ser carregada pelo CLR. Isto infelizmente irá gerar problemas, e espero que em uma versão futura do C# faça as definição das regras do C# mais rígidas ou até mais rígidas que as do CLR. Até lá, evite fazer isso.
Segundo, infelizmente, o compilador de C# atualmente tem vários bugs no seu detector de ciclo, como algumas vezes em que coisas se parecem com ciclos mas não são realmente, e são marcados com erros de ciclos. Isto só torna ainda mais difícil a compreensão do que é um ciclo válido e o que não é. Por exemplo, hoje o compilador irá incorretamente dizer que isto é um ciclo de classe base inválido, mesmo que claramente não é:
public class November<T> { }
public class Romeo : November<Romeo.Sierra.Tango>
{
public class Sierra
{
public class Tango { }
}
}
De qualquer forma, voltando ao assunto: variância louca. Nós temos as interfaces definidas acima, e então damos ao compilador um pequeno enigma pra resolver:
IC<double> bar = qualquer;
IN<IC<string>> foo = bar; // Esta atribuição é válida?
Estamos prestes a entrar em um nome generic praticamente impossível de ler, então pra ficar mais fácil, a partir de agora vamos abreviar IN<IC<string>> como NCS. IC<double> será abreviado como CD.
Do mesmo modo, vou abreviar "é convertível de forma implícita" por uma seta. Então a questão é: CD→NCS é true ou false?
Vamos ver. Claramente CD não se converte em NCS diretamente. Mas (por motivos do compilador) talvez o tipo base de CD’s consiga.
O tipo base de CD’s é NNCCD. Será que NNCCD→NCS? Bem, N é contravariante em seus parâmetros assim isto se resume a questão, CS→NCCD?
Claramente não diretamente. Mas talvez CS tenha um tipo base que converta em NCCD. O tipo base de CS é NNCCS. Agora nós temos a questão NNCCS→NCCD ?
N é contravariante em seus parâmetros, agora caímos em CCD→NCCS ?
O compilador vai "reduzindo" o problema se CD→NCS é verdadeiro à CCD→NCCS! Se mantermos uma "redução" como esta então teremos CCCD→NCCCS, CCCCD→NCCCCS, e assim por diante.
Isto é apenas uma fração de algumas das formas em que o sistema de tipos pode ficar estranho. Para se aprofundar mais nesse assunto, você pode ler o Microsoft Research paper.