Aqui está um exemplo simples usando uma hierarquia de herança.
Dada a hierarquia de classes simples:
E no código:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invariância (ou seja, parâmetros de tipo genérico * não * decorados com in
ou out
palavras-chave)
Aparentemente, um método como este
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... deve aceitar uma coleção heterogênea: (o que faz)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
No entanto, a transmissão de uma coleção de um tipo mais derivado falha!
var myGiraffes = new List<Giraffe>
{
new Giraffe(), // "Jerry"
new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!
cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'
Por quê? Como o parâmetro genérico IList<LifeForm>
não é covariante -
IList<T>
é invariável, IList<LifeForm>
apenas aceita coleções (que implementam IList) onde o tipo parametrizado T
deve estar LifeForm
.
Se a implementação do método PrintLifeForms
era maliciosa (mas tem a mesma assinatura de método), a razão pela qual o compilador impede a passagem List<Giraffe>
se torna óbvia:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Como IList
permite adicionar ou remover elementos, qualquer subclasse de LifeForm
poderia, portanto, ser adicionada ao parâmetro lifeForms
e violaria o tipo de qualquer coleção de tipos derivados passada para o método. (Aqui, o método malicioso poderia tentar adicionar um Zebra
a var myGiraffes
). Felizmente, o compilador nos protege deste perigo.
Covariância (genérico com tipo parametrizado decorado com out
)
A covariância é amplamente usada com coleções imutáveis (ou seja, onde novos elementos não podem ser adicionados ou removidos de uma coleção)
A solução para o exemplo acima é garantir que um tipo de coleção genérica covariante seja usado, por exemplo IEnumerable
(definido como IEnumerable<out T>
). IEnumerable
não possui métodos para alterar a coleção e, como resultado da out
covariância, qualquer coleção com o subtipo de LifeForm
agora pode ser passada para o método:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
agora pode ser chamado com Zebras
, Giraffes
e qualquer IEnumerable<>
de qualquer subclasse deLifeForm
Contravariância (genérico com tipo parametrizado decorado com in
)
Contravariância é freqüentemente usada quando funções são passadas como parâmetros.
Aqui está um exemplo de uma função, que recebe um Action<Zebra>
como parâmetro e a invoca em uma instância conhecida de uma Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Como esperado, isso funciona muito bem:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuitivamente, isso falhará:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
No entanto, isso consegue
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
e até isso também tem sucesso:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Por quê? Porque Action
é definido como Action<in T>
, isto é contravariant
, significa que Action<Zebra> myAction
, para , isso myAction
pode ser "no máximo" a Action<Zebra>
, mas Zebra
também são aceitáveis superclasses de menos derivadas de .
Embora isso possa não ser intuitivo a princípio (por exemplo, como pode Action<object>
ser passado como um parâmetro exigindo Action<Zebra>
?), Se você descompactar as etapas, notará que a função chamada ( PerformZebraAction
) é responsável por transmitir dados (neste caso, uma Zebra
instância ) para a função - os dados não provêm do código de chamada.
Devido à abordagem invertida do uso de funções de ordem superior dessa maneira, no momento em que Action
é invocada, é a Zebra
instância mais derivada invocada contra a zebraAction
função (passada como parâmetro), embora a própria função use um tipo menos derivado.