Eu já havia visto antes, já faz um tempo, e quando vi pensei “Ah, nunca vou ter esse problema.” Pois é, tive. Talvez você também tenha, e como não encontrei muitos recursos em português vai aqui a dica, da maneira mais simplificada que eu consegui:

Herança não funciona com generics. Nem no C#, nem no VB.

– Hã, como assim?

Mais ou menos assim: você tem uma lista de cachorros. Todo cachorro é um mamífero (cachorro herda de mamífero). Você consegue passar uma variável cachorro para uma outra declarada como mamifero:

var cachorro = new Cachorro();

Mamifero mamifero = cachorro;

Nada de mais, certo? E esse código a seguir, funciona? O que você acha?

var cachorros = new List<Cachorro>();

List<Mamifero> mamiferos = cachorros;

Faz sentido que funcionasse, certo? Mas não funciona. Um screenshot do Visual Studio mostra a vocês o erro:

Erro de covariância no Visual Studio (C#)

O erro é “Cannot implicitly convert type ‘System.Collections.Generic.List<InvariantsCSharp.Cachorro>’ to ‘System.Collections.Generic.List<InvariantsCSharp.Mamifero>’”. E nem adianta fazer cast, porque não vai poder. O compilador é muito esperto. E se, de alguma forma, você conseguir, vai dar pau em runtime, na hora da associação. E porquê? Se todo cachorro é um mamífero, uma lista de cachorros é uma lista de mamíferos, certo?

Não. Errado. Na verdade, não é bem uma questão de lógica, mas de especificação. O CLR permitiria isso, quem não permite são o C# e o VB. Primeiro temos que entender que List<Cachorro> não herda de List<Mamifero>. Mesmo se fosse possível, não seria por herança. Entender isso é o primeiro passo.

Em poucas palavras, o motivo disso não ser permitido é o seguinte: Se você pudesse converter uma lista de cachorros para uma lista de mamíferos, poderia, depois, adicionar um gato à lista. E o compilador jamais iria pegar esse problema, você só ia descobrir o problema em tempo de execução. E o grande motivo de existirem os generics é justamente você poder ter segurança do tipo em tempo de compilação. Já imaginou Se isso fosse possível? Dá uma olhada no que poderíamos fazer:

var cachorros = new List<Cachorro>();

List<Mamifero> mamiferos = cachorros;

Gato gato;

mamiferos.Add(gato);

Se isso fosse possível, seria pau na certa. E só em runtime.

Essa especificação, que impede esse tipo de construção, se chama “invariante”. Generics, no C# e no VB, são invariantes.

Mas o problema de listas ainda dá para resolver, só que sem generics. Se você quiser criar uma lista de cachorros, e depois passá-la para uma variável de lista de mamíferos, use um array. A seguinte construção é perfeitamente legal:

Cachorro[] cachorros;

Mamifero[] mamiferos = cachorros;

Agora, já viu se você adicionar um gato nesse array de mamíferos, não é? É pau na hora. O que fizemos foi simplesmente passar um array já criado para outro sendo declarado em um tipo menos específico. Essa possibilidade se chama “covariância”. Também existe a possibilidade de amplicação, chamada contravariância. Penso em possibilidades legítimas de uso, mas tem que ser feito com cuidado. Além disso tudo, arrays são construções que hoje, com os generics, ficaram velhas. Vejam este post do Eric Lippert, feito esta semana, sobre isso. Ele bem nos lembra que arrays não são redimensionáveis, e não são classes, são estruturas, o que nos impede, por exemplo, de deixá-los read-only. Leia o post, é bem legal.

Se a necessidade não for com listas você vai morrer na praia. Não há outra solução. Acreditem, me deparei com isso hoje, e não tem jeito. O compilador não permite em tempo de compilação, e em runtime dá o mesmo pau, se você der um jeito de enganar. Por exemplo, vamos supor esta classe:

class Tranportador<T> where T : Mamifero {}

Ela faz o transporte de mamíferos. Ok, nada de mais. Se você tiver uma função que receba tipado assim:

void Tranportar(Tranportador<Mamifero> transportador) { }

Não consegue fazer isso:

var transportadorDeCachorro = new Tranportador<Cachorro>();

Tranportar(transportadorDeCachorro); //vai dar pau de compilação

Só isso:

var transportadorDeMamifero = new Tranportador<Mamifero>();

Tranportar(transportadorDeMamifero);

Deu para entender o drama?

E como resolve isso? Há uma maneira, mas você vai ter que alterar a função. Fica assim:

static void Tranportar<T>(Tranportador<T> transportador) where T : Mamifero { }

Essa função agora é uma função genérica. Antes a função não era genérica, ela tinha um argumento com o tipo bem definido: Tranportador<Mamifero> transportador. Na prática, a intenção da função anterior está mantida: faça o transporte de algum mamífero.

A chamada à função fica assim:

    1 var transportadorDeCachorro = new Tranportador<Cachorro>();

    2 Tranportar<Cachorro>(transportadorDeCachorro); //sem inferência de tipo

    3 Tranportar(transportadorDeCachorro); //com inferência de tipo

    4 var transportadorDeMamifero = new Tranportador<Mamifero>();

    5 Tranportar<Mamifero>(transportadorDeMamifero); //sem inferência de tipo

    6 Tranportar(transportadorDeMamifero); //com inferência de tipo

Notem que nem precisamos tipar a chamada. Ele já percebe, nas linhas 3 e 6, qual o tipo do método, pelo tipo do argumento.

Vocês já encontraram problemas parecidos? Como resolveram?

Tudo funciona quando você “pensa generics”. Há casos sem solução, mas na prática a lei da invariância é para ajudar.

E se você gostou do assunto, a Wikipedia fala um pouco mais (infelizmente em inglês, sem tradução para o português) e o Rick Byers fez um excelente post sobre o assunto, lá em 2005. Vale a pena dar uma olhada, principalmente no post do Rick. A Wikipedia pega um pouco mais pesado.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.