C# 4.0: Covariância E Contravariância Em Genéricos
O C# 4.0 (e a .NET 4.0) introduziram covariância e contravariância em interfaces e delegates genericos. Mas afinal o que é a variância?
Segundo a Wikipedia, em álgebra multilinear, covariância e contravariância descrevem como a descrição quantitativa de certas entidades geométricas ou físicas variam quando passam de um systema de coordenadas para outro.(*)
Mas o que é que isto tem a ver com C# ou .NET?
Na teoria de tipos, um tipo T é maior (>) que o tipo S se S é um subtipo (deriva) de T, o que quer dizer que existe uma descrição quantitativa para tipos numa hierarquia de tipos.
Sendo assim, como é que a covariância e a contravariância se aplicam aos tipos genéricos do C# (e de .NET)?
Em C# (e em .NET), variância é uma relação entre uma definição de tipo genérico e um determinado tipo parâmetro de genéricos.
Dados dois tipos Base e Derivado, em que:
Uma definição de tipo genérico Genérico<T> é:
-
covariante em T se a ordem dos tipos construídos segue a ordem dos tipos parâmetros de genéricos : Genérico<Base> ≥ Genérico<Derivado>
-
contravariante em T se a ordem dos tipos construídos é inversa à ordem dos tipos parâmetros de genéricos : Genérico<Base> ≤ Genérico<Derivado>
-
invariante em T se nenhuma das regras acima se aplica.
Se aplicarmos a definição a arrays, podemos constatar que os arrays sempre foram covariantes porque este código é válido:
object[] objectArray = new string[] { "string 1", "string 2" };
objectArray[0] = "string 3";
objectArray[1] = new object();
Contudo, quando tentamos correr este código, a segunda afectação vai lançar uma ArrayTypeMismatchException. Apesar do compilador ter sido enganado para pensar que o código era válido porque um object está a ser atribuído a um elemento de um array de object, em tempo de execução, há sempre uma verificação de tipos para garantir que o tipo em tempo de execução da definição dos elementos do array é maior ou igual ao da instância quie está a ser atribuída ao elemento. No exemplo acima, porque o tipo em tempo de execução é array de string, a primeira afectação de elementos é válida porque string ≥ string e a segunda não é válida porque string ≤ object.
Isto leva-nos à conclusão de que, apesar dos arrays sempre terem sido covariantes, não são covariantes sem riscos – não é garantido que código que compila corra sem erros.
Em C#, a variância em relação um determinado tipo parâmetro de genéricos é forçada na declaração e não determinada pelo uso desse tipo parâmero de genéricos.
A covariância em relação a um determinado tipo parâmetro de genéricos é forçada através do modificador genérico out:
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T>
{
T Current { get; }
bool MoveNext();
}
Note-se o conveniente uso da palavra reservada out já existente. Além do benefício de não nos termos de lembrar de uma nova hipotética palavra reservada covariant, out (saída) é mais fácil de lembrar porque define que o tipo parâmetro de genéricos apenas pode ser usando em posições de saída — propriedades apenas de leitura e valores de saída de métodos.
Similarmente, a contravariância em relação a um determinado tipo parâmetro de genéricos é forçada através do modificador genérico in:
public interface IComparer<in T>
{
int Compare(T x, T y);
}
Mais uma vez, o uso da palavra reservada in (entrada) já existente é mais fácil de lembrar keyporque define que o tipo parâmetro de genéricos apenas pode ser usando em posições de entrada — propriedades apenas de escrita e parâmetros de métodos que não sejam por referência (ref) nem de saída (out).
Um tipo parâmetro de genéricos que não seja marcado covariante (out) ou contravariante (in) é invariante.
Porque a variância se aplica à relação entre uma definição de tipo genérico e um determinado tipo parâmetro de genéricos, uma definição de tipo genérico pode ser covariante, contravariante, invariante dependendo do tipo parâmetro genérico.
public delegate TResult Func<in T, out TResult>(T arg);
Na definição do delegate genérico acima, Func<T, TResult> é contravariant em T e convariante em TResult.
Todos os tipos da .NET Framework onde a variância podia ser aplicada foram alterados para que os seus tipos parâmetros de genéricos pudessem tirar partido desta funcionalidade.
Em resumo, as regras da variância em C# (e .NET) são:
-
A variância em relação a tipos parâmetros de genéricos está restringida aos tipos interfaces genéricas e delegates genéricos.
-
Uma interface genérica ou um delegate genérico podem ser, em simultâneo covariante, contravariante ou invariante em relação a diferentes tipos parâmetros de genéricos.
-
A variância aplica-se apenas a tipos referência: um IEnumerable<int> não é um IEnumerable<object>.
-
A variância não se aplica à combinação de delegates. Isto é, dados dois delegates do tipo Action<Derived> e Action<Base>, não é possível combinar o segundo delegate com o primeiro apesar de que o resulado ser seguro do ponto de vista do tipo. A variância permite que o segundo delegate seja atribuído a uma variável do tipo Action<Derived>, mas os delegates apenas podem ser combinados se os seus tipos forem uma correspondência exacta.
Quem desejar aprender mais acerca da variância em C# (e .NET), pode ler:
Nota: Por a variância ser uma funcionalidade da .NET 4.0 e não apenas do C# 4.0, estes conceitos também se aplicam ao Visual Basic 10.