Primeiro precisamos definir o que seria variância e suas filhas covariância e contravariância. Para começar, você precisa entender que os sistemas de tipos da maioria das linguagens de programação orientada a objetos aceitam que tipos novos sejam criados a partir de outros usando herança, e que estes tipos novos podem ser usados em qualquer lugar onde se esperaria os tipos bases.

Herança simples

Exemplo:

Se você tem uma classe Gato que herda de Animal, poderá usá-la em qualquer método que aceite apenas Animal como parâmetro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Animal
{
	public abstract void BalanceRabo();
}
public class Gato : Animal
{
	public override void BalanceRabo()
	{
		System.Console.WriteLine("abalançando freneticamente");
	}
}
public static void Chame(Animal animal)
{
	animal.BalanceRabo();
}
public static void Main(string[] args)
{
	var gato = new Gato();
	Chame(gato);// <<--compila sem problemas
}

Este comportamento é chamado de Polimorfismo e provavelmente você já deve ter ouvido falar, mas o que fazer se o método ao invés de apenas um Animal esperasse uma lista de Animal?

Quando estamos trabalhando com containers de tipos, traduzindo: classes que só existem como uma espécie de caixas moldadas exclusivamente para acesso a tipos específicos, estas metamorfoses passam a ser chamadas de variâncias e são classificadas em três subgrupos covariância, contravariância e invariância.

Agora que você já sabe que isso existe, vamos nos aprofundar um pouco, veja o exemplo a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class Animal
{
	public abstract void BalanceRabo();
}
public class Gato : Animal
{
	public override void BalanceRabo()
	{
		System.Console.WriteLine("abalançando freneticamente");
	}
}

public static void Main(string[] args)
{
	Gato[] gatos = new Gato[] { new Gato(), new Gato()};
	Chame(gatos);// <<--gatos que é do tipo Gato[] é convertido para Animal[], tipo mais genérico
}

public static void Chame(Animal[] animais)
{
	foreach(var animal in animais)
		animal.BalanceRabo();
}

No exemplo acima, note que o tipo System.Array, container para os tipos Animal e Gato, permite sua metamorfose sempre que o tipo que ele guarda é um tipo base da outra System.Array, damos o nome dessa mutação de covariância.

Sentido covariancia

Trocando em miúdos, temos que covariância ocorre sempre quando um objeto container inicializado com um tipo mais especializado pode ser assinalado a um objeto container que possui um mais básico.

Já a Contravariância é o contrário, onde apenas tipos especializados aceitam tipos mais básicos.

Sentindo contravariancia

Parece não fazer sentido, mas faz e você já deve ter utilizado contravariância no seu dia-a-dia, veja este exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void Log(object data)
{
	Console.WriteLine($"Called with {data} type of {data.GetType().Name}");
}

public class Writter
{
	private string _data;
	public Writter(string data)
	{
		_data = data;
	}
	public  override string ToString()
	{
		return $"Writter says '{_data}'";
	}
}

public static void Main(string[] args)
{
	Action<string> callLogAsString = Log; //<-- Log é do tipo Action<Object> convertido para Action<string>, note que string é mais especifico que object.
	Action<Writter> callLogAsWritter = Log;
	
	callLogAsString("foo");
	callLogAsWritter(new Writter("bar"));
	
}

No exemplo acima, a partir da assinatura de um método genérico, foi possível converte-lo para tipo mais específico.

Agora vamos ver em que problemas estes tipos de metamorfoses podem nos levar. No primeiro exemplo, vimos que System.Array é covariante, tornando possível que código seguinte possa ser compilado, mas falhe miseravelmente em tempo de execução:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Camelo : Animal
{
	public override void BalanceRabo()
	{
		System.Console.WriteLine("abalançando levemente");
	}
	public void BebaAgua()
	{
		System.Console.WriteLine("Bebendo 5 litros de água");
	}
}
public static void Chame(Animal[] animais)
{
	foreach(var animal in animais)
		animal.BalanceRabo();
animais[0] = new Camelo();//<--aqui terei um erro em tempo de execução (ArrayTypeMismatchException)
}

Note que é seguro ler as propriedades e chamar métodos de containers covariantes, porém perigoso escrever. No exemplo acima, o método Chame recebeu um Array de Gatos, mas tentou escrever um Camelo que obviamente não cabe, gerando erro em tempo de execução.

No C# a partir da versão 4 podemos controlar a variâncias dos nossos tipos a partir das palavras chaves in e out.

Este controle pode ser feito apenas a partir de interfaces genéricas ou delegates genéricos, não sendo permitido em classes e outros tipos, a forma como os tipos são expostos também são controlados, neste último caso para garantir a segurança dos dados expostos pelos containers. Veja: A palavra chave in, define tipos que são contravariantes.

1
2
3
4
5
6
7
public interface IKlay<in T>
{
	void Receba(T algo);
}

IKlay<Camelo> klayEspecifico = new Klay<Animal>();
klayEspecifico.Receba(new Gato());

Embora sejam de tipos diferentes é seguro já que o método só conhece animais (de quem gato é derivado) e não esqueça que quem realizará o trabalho será Klay e não Klay. A palavra out, define tipos covariantes que só são seguros quando lidos dos containers. Por isso, só é possível definir tipos covariantes como retorno de métodos:

1
2
3
4
5
6
public interface IKlay<out T>
{
	T Envie();
}
IKlay<Animal > klayGenerico  = new Klay< Camelo >();
Animal enviado = klayGenerico.Envie();

Totalmente seguro ler o tipo enviado pelo tipo genérico covariante.