Covariância e Contravariância

5 de maio de 2022
Bryan Lima

Coder and Tech Consultant

  • Twitter
  • Facebook
  • LinkedIn
Covariância e Contravariância

Em C#, a covariância e a contravariância habilitam a conversão de referência implícita para tipos de array, tipos de delegates e argumentos de tipo genérico. A covariância preserva a compatibilidade de atribuição, e a contravariância reverte. [Microsoft .NET C# Guide]

Baseado na definição da Microsoft para o .NET pode identificar que três tipos têm o conceito de covariância e contravariância. São eles:

  • Arrays
  • Delegates
  • Argumentos de tipo genérico, famoso <T>

Durante as explicações vou escrever que o tipo é mais ou menos derivado, isso porque essa definição serve tanto para explicar herança de tipos e derivações em delegates porque não existe herança em delegates, apenas derivações.

Antes de começar será utilizado as seguintes classes durante todos os exemplos

C#
class Pessoa {}
class Estudante : Pessoa {}

O que é Covariante ?

Covariante permite que se use um tipo mais derivado do que originalmente foi especificado.

Significa que pode utilizar um subtipo B onde é esperado um tipo A, desde que B derive de A.

Confusa essa frase ? Basicamente é a famosa herança de tipos. Alterando utilizando as classes Pessoa e Estudante ficaria assim.

Significa que pode utilizar um subtipo Estudante onde é esperado um tipo Pessoa, desde que Estudante derive de Pessoa.

Pode usar uma instância de IEnumerable<Estudante> onde se espera IEnumerable<Pessoa> porque o argumento T em IEnumerable<T> é contravariante.

No geral, um tipo covariante pode ser usado como um tipo de retorno em um método da interface genérica ou delegate genérico.

O que é Contravariante ?

Contravariante permite tipos menos derivados(mais genéricos) do que originalmente foi especificado.

Significa que pode utilizar um tipo A onde é esperado um subtipo B, desde que B derive de A.

Por exemplo, a classe Estudante é um subtipo de Pessoa, então a classe Pessoa pode ser utilizada no lugar de Estudante.

Pode usar uma instância de Action<Pessoa> onde se espera Action<Estudante> porque o argumento T em Action<T> é contravariante.

No geral, um tipo contravariante pode ser utilizado como argumento de um método da interface genérica ou delegate genérico.

Covariância em Generics

Uma interface de covariante permite que seus métodos retornem mais tipos derivados daquelas especificadas na interface.

Em .NET, é utilizada a palavra chave out no argumento genérico, famoso T, isso significa que o método pode retornar qualquer subtipo que derive de T, mas ele não pode ser utilizado como argumento de um método.

C#
interface IOperation<out T> // repare na palavra out no argumento
{
	T Execute();
	// void Execute(T value); // Erro de invariância porque o tipo T covariante não pode ser utilizado como argumento.
}

IOperation<Pessoa> op = (IOperation<Estudante>)null; // Usando Estudante onde espera Pessoa

A o tipo Estudante é um subtipo de Pessoa, então o tipo Estudante pode ser utilizado no lugar do tipo Pessoa.

Contravariância em Generics

Uma interface de contravariante permite que seus métodos aceitem parâmetros de tipos menos derivados do que os especificados na interface.

Em .NET, é utilizada a palavra chave in no argumento genérico, famoso T, isso significa que pode ser utilizado qualquer tipo que T deriva como argumento de um método, mas não como retorno.

C#
interface IOperation<in T>  // repare na palavra in no argumento
{
	// T Execute(); // Erro de invariância porque o tipo T contravariante não pode ser utilizado como retorno.
	void Execute(T value);
}

IOperation<Estudante> op = (IOperation<Pessoa>)null; // Usando Pessoa onde espera Estudante

O tipo Estudante é um subtipo de Pessoa, então o tipo Pessoa pode ser utilizado no lugar do tipo Estudante.

Covariância e Contravariância em Generic Delegates

As mesmas regras de covariância e contravariância em interface genérica podem ser aplicadas em delegates genéricos.

As mesmas regras de in e out são aplicadas nos delegates genéricos.

C#
public delegate T CadastrarEmCurso<out T>(); // Covariante
public delegate void Notificar<in T>(T valor); // Contravariante

// Covariância
CadastrarEmCurso<Pessoa> d1 => () => new Estudante();

// Contravariância
void NotificarPessoa(Pessoa pessoa) { };
Notificar<Estudante> d2 = NotificarPessoa;

Mais sobre delegates

Também é possível utilizar covariância e contravariância no mesmo delegate, mas com parâmetros diferentes.

C#
public delegate TResult Notificar<in T, out TResult>(T valor);

Existem dois delegates muito utilizados no .NET que são Func<T> e Action<T>, ambos são generic delegates invariantes que utilizam in e out nos argumentos genéricos da mesma forma que foi visto anteriormente.

Olhe como os códigos fontes de Func<T> e Action<T> são parecidos com o que foi exibido aqui.

C#
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

Covariância em Arrays ou Generic Collections

C#
public void ExibirNomes(Pessoa[] pessoas)
{
	foreach (var pessoa in pessoas)
	{
		Console.WriteLine(pessoa.Nome);
	}
}

Estudante[] estudantes =
{
	new Estudante("Alice"),
	new Estudante("Bob")
};
ExibirNomes(estudantes);

Tudo funciona certo como visto anteriormente. Mas o que acontece se substituir um Estudante por Pessoa ?

C#
public void Atualizar(Pessoa[] pessoas)
{
	pessoas[0] = new Pessoa("Cindy");
}
Estudante[] estudantes =
{
	new Estudante("Alice"),
	new Estudante("Bob")
};
Atualizar(estudantes);

Como observado, o código compila acima, mas ocorre o erro em runtime porque o tipo que está tentando atualizar é diferente do tipo do array.

Contravariância em Arrays ou Generic Collections

.NET não tem suporte para contravariância implícita em arrays e coleções genéricas.

O que mais é utilizado nesse caso são derivações de comparadores como IComparer<T>, IEqualityComparer<T> ou IComparable<T>.

Observações

Para habilitar a conversão implícita, é necessário declarar explicitamente os parâmetros genéricos, famoso T, em um delegado ou interface como covariante ou contravariante usando a palavra-chave in ou out.

Use out para covariância quando precisar que os métodos retornem tipos mais derivados, ou seja, subtipos.

Use in para contravariância quando precisar que os parâmetros aceitem tipos menos derivados, ou seja, tipos que o subtipo deriva.

Referências

  1. Microsoft .NET C# Guide - Covariance and Contravariance
PUBLICADO EM: