Ecco un semplice esempio usando una gerarchia ereditaria.
Data la semplice gerarchia di classi:
E nel codice:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invarianza (ovvero parametri di tipo generico * non * decorato con in
o out
parole chiave)
Apparentemente, un metodo come questo
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... dovrebbe accettare una raccolta eterogenea: (cosa che fa)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Tuttavia, il passaggio di una raccolta di un tipo più derivato non riesce!
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>'
Perché? Poiché il parametro generico IList<LifeForm>
non è covariante -
IList<T>
è invariante, quindi IList<LifeForm>
accetta solo raccolte (che implementano IList) dove T
deve essere il tipo con parametri LifeForm
.
Se l'implementazione del metodo PrintLifeForms
era dannosa (ma ha la stessa firma del metodo), il motivo per cui il compilatore impedisce il passaggio List<Giraffe>
diventa ovvio:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Poiché IList
consente l'aggiunta o la rimozione di elementi, qualsiasi sottoclasse di LifeForm
potrebbe quindi essere aggiunta al parametro lifeForms
e violerebbe il tipo di qualsiasi raccolta di tipi derivati passati al metodo. (Qui, il metodo dannoso tenterebbe di aggiungere un Zebra
a var myGiraffes
). Fortunatamente, il compilatore ci protegge da questo pericolo.
Covarianza (Generico con tipo parametrico decorato con out
)
La covarianza è ampiamente utilizzata con raccolte immutabili (ovvero dove nuovi elementi non possono essere aggiunti o rimossi da una raccolta)
La soluzione all'esempio precedente è garantire che venga utilizzato un tipo di raccolta generico covariante, ad esempio IEnumerable
(definito come IEnumerable<out T>
). IEnumerable
non ha metodi per modificare la raccolta e, a causa della out
covarianza, qualsiasi raccolta con sottotipo di LifeForm
può ora essere passata al metodo:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
può ora essere chiamato con Zebras
, Giraffes
e qualsiasi IEnumerable<>
di qualsiasi sottoclasse diLifeForm
Contravarianza (Generico con tipo parametrico decorato con in
)
La contraddizione viene spesso utilizzata quando le funzioni vengono passate come parametri.
Ecco un esempio di una funzione, che accetta un Action<Zebra>
come parametro e lo richiama su un'istanza nota di una Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Come previsto, funziona perfettamente:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuitivamente, questo fallirà:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Tuttavia, questo ha successo
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
e anche questo riesce anche:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Perché? Perché Action
è definito come Action<in T>
, cioè lo è contravariant
, nel senso che per Action<Zebra> myAction
, che myAction
può essere al massimo "a" Action<Zebra>
, ma Zebra
sono accettabili anche superclassi di meno derivate .
Anche se all'inizio potrebbe non essere intuitivo (ad es. Come si può Action<object>
passare come parametro che richiede Action<Zebra>
?), Se si scompattano i passaggi, si noterà che la funzione chiamata ( PerformZebraAction
) stessa è responsabile del passaggio dei dati (in questo caso Zebra
un'istanza ) alla funzione: i dati non provengono dal codice chiamante.
A causa dell'approccio invertito di utilizzare le funzioni di ordine superiore in questo modo, quando Action
viene invocata, è l' Zebra
istanza più derivata che viene invocata contro la zebraAction
funzione (passata come parametro), sebbene la funzione stessa utilizzi un tipo meno derivato.