Continuo trazendo as novidades de C# 4.0. Este é o terceiro post, se você não viu os posts anteriores:
- C# 4.0 – Quanto antes melhor – onde apresentei um resumo das novidades, além de assuntos que envolvem a nova versão da linguagem;
- C# 4.0: Uma linguagem dinâmica – onde apresentei as novidades do C# 4.0 que vão aproximá-lo mais de uma linguagem dinâmicas.
Ou ainda pela tag: C#4.
Aviso: Antes de continuar, vou contar novamente a história triste (já contada nos posts anteriores sobre o mesmo assunto). Os bits que estão disponíveis para download não são tão quentes quanto os bits apresentados no PDC. Sorry, mas é verdade. Isso quer dizer que algumas das coisas que vou mostrar aqui não vão compilar no build do CTP. Sabendo disso, vamos lá.
Neste post vou falar de covariância e contravariância, o que finalmente vai por um fim à invariância predominante no C# (fora os arrays que já são covariantes). É o segundo grande assunto sendo abordado porque é o segundo mais legal e interessante na minha opinião. A princípio é meio contra-intuitivo pensar sobre variância, mas depois que sua mente já deu uns nós ela volta ao normal e de repente você começa a entender. Se acontecer com você, não se preocupe, é assim mesmo, é normal.
Para entender um pouco melhor o drama da variância, dêem uma lida em uma reclamação minha sobre o problema gerado por ela aqui no blog. Vale a pena também ver o trabalho do Felipe Pessoto, que traduziu uns posts do Eric Lippert (do time do C#, e um ótimo blogger) que tratavam muito bem sobre o assunto. Se você não conhece o assunto eu recomendo a leitura.
Lembrando então rapidamente> Arrays são covariantes. Isso significa que você pode pegar um array de strings:
string[] textos;
E tipá-lo como um array de objetos, porque string herda de objeto:
object[] objetos = textos; //compila sem problemas
Mas tipos genéricos são invariantes. Isso quer dizer que se você tiver uma lista de strings:
IList<string> textos = new List<string>();
Você não consegue passá-la para uma lista de objetos:
IList<object> objetos = textos; //não vai compilar no C# 3
Ainda que toda string seja um objeto também, o problema acontece porque IList<string> não herda de IList<object>, e portanto a associação não é permitida. Se fosse, você poderia quebrar o tipo genérico em runtime assim:
IList<object> objetos = textos; objetos.Add(new Button()); //pau em runtime: um botão não é uma string
De fato, é exatamente o que pode acontecer com arrays. Seguindo no exemplo, se você fizer isso:
objetos[0] = new Button();
Vai compilar porque um botão é um objeto, mas em runtime você vai ter um exceção sendo lançada, porque um botão não é uma string, e na verdade o array é originalmente um array de strings, apenas tipado como um array de objetos. Perigoso, certo?
Ok, mas imaginem que tivéssemos um enumerador de strings:
IEnumerable<string> textos = ObterUmEnumeradorDeStrings();
Não seria tão perigoso assim associar este enumerador a um enumerador de objetos:
IEnumerable<objects> objetos = textos;
Porque, na verdade, não temos como alterar os tipos contidos pelo enumerador, ele é uma coleção em que os itens só podem ser lidos e iterados, e é por isso somente-leitura. Esse tipo de conversão não é perigosa, e não teria problema algum em ser permitida, certo?
Na verdade, o que acontece com IEnumerable<T>, é que este tipo genérico apenas devolve o tipo T em operações, ele não o recebe como parâmetro em nenhum momento, o que elimina a necessidade de qualquer conversão. Sua assinatura é assim:
public interface IEnumerable <T>: IEnumerable
{
IEnumerator<T> GetEnumerator();
}
É diferente de IList, que recebe T como parâmetro, por exemplo, nos métodos Insert e IndexOf (além de outros das interfaces que IList implementa):
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
Por este motivo, no caso de IList, o objeto não seria covariante com segurança, porque se isso fosse possível, um IList<object> poderia, no fundo, ser um IList<string> e receber uma chamada de Insert(0, new Button()), o que resultaria em um erro em runtime.
Para indicar este tipo de situação em tempo de compilação, o time do C# incluiu duas novas palavra-chave opcionais na definição de generics: “out” e “in”. “Out” é para ser usado quando o tipo genérico é covariante, e pode ser retornado em alguma função ou propriedade readonly, como no caso do IEnumerable. “In” é para quando o tipo genérico é contravariante, e pode ser recebido como parâmetro.
Assim, a interface IEnumerable na nova versão ficaria assim:
public interface IEnumerable <out T>: IEnumerable
{
IEnumerator<T> GetEnumerator();
}
E você poderia então fazer isso:
IEnumerable<string> textos = ObterEnumerador(); IEnumerable<object> objetos = textos; //vai compilar no C# 4
Porque o compilador sabe que é seguro. Um enumerador de strings é sempre um enumerador de objetos e não há perigo em tratá-lo pelo tipo menos específico (objeto). Muito inteligente, certo?
Um exemplo de contravariância é uma interface que só recebe parâmetro de entrada. O exemplo dado pela própria Microsoft foi a interface IComparer. Originalmente assim:
public interface IComparer<T>
{
int Compare(T x, T y);
}
Ela poderia ser alterada com a palavra chave “In”, e ficaria assim:
public interface IComparer<in T>
{
int Compare(T x, T y);
}
O que permitiria isso, que até o momento não é permitido no C# 3.0:
IComparer<object> comparadorObjetos; IComparer<string> comparadorStrings = comparadorObjetos;
Isso porque uma classe que é capaz de comparar objetos, também é capaz de comparar strings. Por esse motivo, a interface é marcada como genérica e contravariante, ou seja, aceita tipos genéricos apenas como entrada em formato de algum parâmetro, mas não como saída (de uma função, por exemplo).
Isso conclui o básico sobre a novidade de variância. Faço apenas três observações:
- Assim como no caso dos tipos dinâmicos, a máquina virtual disponibilizada para download pela Microsoft com Visual Studio 2010, Rosario, C# 4.0 e .Net 4.0 não contém ainda os bits necessários para fazer essa sintaxe compilar. Isso quer dizer que não dá ainda para criar tipos variantes em C# 4.0 com essa VM, e não há os tipos IEnumerable<out T>, ou IComparer<in T> no BCL. Vamos ter que esperar a próxima verão sair, e, depois de todo esse auê com C# 4.0, espero que não demore tanto.
- O segundo aviso é que tipos de valor, como tipos primitivos e structures, são sempre invariantes, ou seja, não dá para fazer cast de um IEnumerable<DateTime> para IEnumerable<object>.
- O terceiro aviso é que parâmetros passados como “out” ou “ref” não podem ser variantes, porque eles seriam “in” e “out”, e acabam não sendo nenhum dos dois.
Boa parte do .Net Framework 4.0 foi alterado para acomodar essa grande mudança. Além dos já citados IEnumerable e IComparer, também há outros, como IQueryable<out T>, e também delegates, como Func<in T, out R>, Action<in T>, e Predicate<in T>. Notem que Func tem um tipo genérico “in” e outro “out”. Isso tem cara que vai deixar muita gente confusa… No fim das contas, a maior parte das pessoas vai simplesmente usar e nem se preocupar como isso tudo está funcionando.
O negócio vai ser tão automático que, quando estivermos trabalhando com C# 9.0, em, sei lá, 2020, alguém vai falar algo do tipo:
- Lembra quando C# era totalmente invariante com generics?
E algum newbie vai dizer:
- C# já foi invariante?
Ah… o futuro…