C# 4.0: Teremos covariância e contravariância

Continuo trazendo as novidades de C# 4.0. Este é o terceiro post, se você não viu os posts anteriores:

  1. C# 4.0 – Quanto antes melhor – onde apresentei um resumo das novidades, além de assuntos que envolvem a nova versão da linguagem;
  2. 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:

  1. 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.
  2. 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>.
  3. 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…

  • Paulo

    Isto pode ser feito pos lista ligada ex:

    class ListaObjetos
    {
    public Object objeto { get; set; }
    public ListaObjetos proxItem { get; set; }
    }

  • http://unplugged.giggio.net/ Giovanni Bassi

    Não entendi. Como assim?

  • http://discountedwatches.biz/ Kenneth Cole

    I don’t like your template but your posts are quite good so I will check back!

  • http://discountedwatches.biz/ Roberto Cavalli

    Do you accept guest posts? I would love to write couple articles here.

  • http://theluxurywatchstore.biz/tag-heuer/tag-heuer-mens-formula-1-indy-500-grande-date-chronograph-watch-cah101b-ba0854/ Tag Heuer Mens Formula

    Please let me know if you are interested to work as article writer for me? I can offer $10/article.

  • http://theswisswatch.biz/seiko/seiko-mens-stainless-steel-chronograph-watch-snd193/ Seiko Mens Stainless Steel

    Very interesting topic will bookmark your site to check if you write more about in the future.