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…
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.