Press enter to see results or esc to cancel.

Desmistificando Covariância e Contravariância em C#

Se existe uma funcionalidade pouco explorada no ambiente .NET é a de generalizar interfaces ou delegates utilizando de covariância e contravariância.

O que é?

Esse recurso foi adicionado no C# 4 e está intimamente ligada na forma que tratamos tipos genéricos, vamos partir de exemplos para  ajudar a demonstrar o problema que esse recurso veio resolver.

Imagine que temos uma classe Banana que herde de uma classe Fruta, podemos ter em algum lugar um método simples que receba um IEnumerable<Fruta>, itere sobre ele e exiba seus nomes.

Primeiro vamos definir a classe base:

public class Fruta
{
    public string Nome { get; set; }

    public Fruta(string nome) 
    {
        Nome = nome;
    }
}

E no programa principal um método que exibe os nomes de qualquer coleção de frutas:

public static void ExibirFrutas(IEnumerable<Fruta> frutas)
{
    foreach (var fruta in frutas)
        Console.WriteLine($"A fruta é: {fruta.Nome}");
}

A partir desse ponto podemos definir uma coleção de frutas e chamar nosso método ExibirFrutas

public static void Main() {
    var frutas = new[] {
        new Fruta("Banana"),
        new Fruta("Maçã"),
        new Fruta("Tomate"),
    };
    ExibirFrutas(frutas);
}

Até aqui tudo bem, compila e funciona da forma que esperamos, sem surpresas.

Vamos agora fazer uma pequena mudança, primeiro criar uma classe derivada de fruta:

public class Banana : Fruta
{
    public Banana(string nome) : base(nome){}
}

E vamos substituir a criação da coleção que irá ser usada para invocar o método ExibirFrutas

public static void Main() {
    var frutas = new[] {
        new Banana("Banana Maçã"),
        new Banana("Banana Nanica"),
        new Banana("Banana Prata"),
    };
    ExibirFrutas(frutas);
}

Note que não alteramos a assinatura do método ExibirFrutas, continua sendo void ExibirFrutas(IEnumerable<Fruta> frutas), nota-se aqui que o que estamos recebendo é um enumerado com tipo genérico de uma classe mais baixa em hierarquia, esse é o comportamento padrão quando tratamos de classes bases e derivadas, ou seja, posso usar classes derivadas aonde se espera classes bases.

Porém nesse exemplo é um pouco diferente, pois estamos tratando de parâmetros genéricos, nossas classes estão envelopadas em um IEnumerable<T>, de fato antes do C# 4, esta mudança que fizemos implicaria que nosso código não iria nem compilar, reclamando que o uso de IEnumerable<Banana> não poderia ser utilizado no lugar de IEnumerable<Fruta> nesta versão.

Esse código no C# moderno funciona sem problema, isso se dá ao fato da interface IEnumerable ser covariante, o que permite que eu utilize tipos derivados do tipo genérico que utilizei para definir meu IEnumerable, assim a assinatura atual do mesmo é:

public interface IEnumerable<out T> { ... }

O que diz ao compilador que esse comportamento é desejado é essa keyword out, ela define um tipo genérico em uma interface ou delegate como covariante.

Qual a diferença entre covariância e contravariância?

Vamos falar de teoria dos conjuntos, mas nada muito complexo, covariância e contravariância são propriedades de uma função de mapeamento que associa um membro de um conjunto a outro.

Considere os dois subconjuntos seguintes do conjunto de todos os tipos do C#.

Primeiro:

{ Animal,
Tigre,
Fruta,
Banana }.

Segundo:

{ IEnumerable<Animal>, 
  IEnumerable<Tigre>, 
  IEnumerable<Fruta>, 
  IEnumerable<Banana> }

Há claramente uma operação de mapeamento do primeiro conjunto para o segundo conjunto. Sendo que para cada T no primeiro conjunto tem um tipo correspondente no segundo conjunto que é IEnumerable<T>. resumindo, o mapeamento é T → IEnumerable<T>. (observe que esta (→) é uma “seta magra” que indica mapeamento).

Agora vamos considerar as relações. Existe uma relação de compatibilidade de atribuição entre pares de tipos no primeiro conjunto. O que simplificando significa que posso usar alguns tipo no lugar de outros (por causa da herança).

Um valor do tipo Tigre pode ser atribuído a uma variável do tipo Animal, portanto, esses tipos são considerados “compatíveis em atribuição”.
Vamos escrever “um valor do tipo X pode ser atribuído a uma variável do tipo Y” da seguinte forma X ⇒ Y. (note que esta (⇒) é uma “seta gorda” que significa compatibilidade em atribuição).

Então, em nosso primeiro subconjunto, aqui estão todos os relacionamentos de compatibilidade de atribuição possiveis:

Tigre ⇒ Tigre
Tigre ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruta
Fruta ⇒ Fruta

Desdo C# 4, é suportada a compatibilidade de atribuição covariante de certas interfaces, sendo assim existe uma relação de compatibilidade entre pares de tipos no segundo conjunto também:

IEnumerable<Tigre>  ⇒ IEnumerable<Tigre>
IEnumerable<Tigre>  ⇒ IEnumerable<Animal>
IEnumerable<Animal> ⇒ IEnumerable<Animal>
IEnumerable<Banana> ⇒ IEnumerable<Banana>
IEnumerable<Banana> ⇒ IEnumerable<Fruta>
IEnumerable<Fruta>  ⇒ IEnumerable<Fruta>

Observe que o mapeamento T → IEnumerable<T> preserva a existência e a direção da compatibilidade da atribuição.
Isto é, se X ⇒ Y, então também é verdade que IEnumerable<X> ⇒ IEnumerable<Y>.

Um mapeamento que possui essa propriedade em relação a uma determinada relação é chamado de “mapeamento covariante“.

Isso deve fazer sentido: uma sequência de tigres pode ser usada onde é necessario uma sequência de animais, mas o oposto não é verdade.

Isso é covariância.

Agora vamos entender contravariância. Considere este subconjunto do conjunto de todos os tipos do C#:

{ IComparable<Tigre>, 
  IComparable<Animal>, 
  IComparable<Fruta>, 
  IComparable<Banana> }

Agora temos o mapeamento do primeiro conjunto para um terceiro conjunto T → IComparable.

IComparable<Tigre> ⇒ IComparable<Tigre>
IComparable<Animal> ⇒ IComparable<Tigre> // invertido!
IComparable<Animal> ⇒ IComparable<Animal>
IComparable<Banana> ⇒ IComparable<Banana>
IComparable<Fruta> ⇒ IComparable<Banana>  // invertido!
IComparable<Fruta> ⇒ IComparable<Fruta>

Ou seja, o mapeamento T → IComparable<T> preservou a existência, mas inverteu a direção da compatibilidade da atribuição. Isto é, se X ⇒ Y, então IComparable<X> ⇐ IComparable<Y>.

Um mapeamento que preserva mas inverte uma relação é chamado de “mapeamento contravariante“.

Sendo assim, fica claro que algo que pode comparar dois animais também pode comparar dois tigres, mas algo que pode comparar dois tigres não pode necessariamente comparar dois animais.

Diferente do IEnumerable a assinatura do IComparable utiliza a keyword in em seu parâmetro genérico.

public interface IComparable<in T> { ... } 

Essa é a diferença entre covariância e contravariância. Covariância preserva a direção de atribuição e a contravariância a inverte.

Por que “in” e “out”?

Vamos olhar novamente, agora de forma completa as interfaces:

public interface IEnumerable<out T> 
{
    IEnumerator<T> GetEnumerator();
}

e

public interface IComparable<in T>
{
    int CompareTo(T other);
}

Note que no IEnumerable seu tipo genérico está sendo utilizado apenas como saída, por isso a keyword out, e no IComparable aonde é aplicado a keyword in o tipo genérico é utilizado apenas para entrada, é exatamente essa a semântica do ‘in’ e ‘out’ respectivamente para contravariância e covariância.

Exemplos de Covariância: IEnumerable<out T>, Func<out T>
Você pode converter de IEnumerable<string> para IEnumerable<object>, ou Func<string> para Func<object>. Isso por que os valores só saem desses objetos. Isso funciona porque, se você está tirando valores da API e retornando algo específico (como string), pode tratar esses valores retornados como um tipo mais geral (como objeto).

Exemplos Contravariância: IComparer<in T>, Action<in T>
Você pode converter de IComparer<object> para IComparer<string> ou Action<object> para Action<string>, os valores só entram nesses objetos.
Desta vez funciona porque se a API sabe tratar algo geral (como objeto) você pode passar algo mais específico (como string) que ela também saberá.

De forma geral, se você tiver uma interface IFoo<T>, ela pode ser covariante em T (isto é, declarar como IFoo<out T> se T for usado apenas como saida (por exemplo, um tipo de retorno) dentro da interface. E ser contravariante em T (isto é, IFoo<in T>) se T for usado apenas em uma posição de entrada (por exemplo, um tipo de parâmetro).

 

Vale ressaltar que um tipo não pode ser ao mesmo tempo covariante e contravariante, isso se dá pelo motivo de que o compilador irá se perder no tipo genérico e causar certas situações bizarras como:

public interface IFoo<in out T> // absurdo!
{
    T Bar(T baz);
}

public class Foo<T> : IFoo<T>
{
   public T Value;
   T Bar(T baz);
}

public static void Main()
{
    var a = new Foo<string>();
    IFoo<object> b = a; //covariância 
    IFoo<int[]> c = b; // contravariância
    
    Console.WrileLine(c.Value); // ?????
}

Qual o valor que deveria ser printado? Não dá para saber, por isso essas duas propriedades não podem existir mutuamente.

Conclusão

Covariância e Contravariância são ferramentas poderosas e pouco exploradas, sempre devemos considera-las quando estamos trabalhando com tipo genéricos.

Tweet about this on TwitterShare on FacebookShare on LinkedInEmail this to someone
Tags

Comments

Leave a Comment