Cleiton Loiola bio photo

Cleiton Loiola

Software design is an art, and like any art it cannot be taught and learned as precise science, by means of theorems and formulas.

Facebook Github Stackoverflow

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.