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 ino outparole 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 Tdeve essere il tipo con parametri LifeForm.
Se l'implementazione del metodo PrintLifeFormsera 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é IListconsente l'aggiunta o la rimozione di elementi, qualsiasi sottoclasse di LifeFormpotrebbe quindi essere aggiunta al parametro lifeFormse violerebbe il tipo di qualsiasi raccolta di tipi derivati passati al metodo. (Qui, il metodo dannoso tenterebbe di aggiungere un Zebraa 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>). IEnumerablenon ha metodi per modificare la raccolta e, a causa della outcovarianza, qualsiasi raccolta con sottotipo di LifeFormpuò ora essere passata al metodo:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeFormspuò ora essere chiamato con Zebras, Giraffese 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 myActionpuò essere al massimo "a" Action<Zebra>, ma Zebrasono 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 Zebraun'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 Actionviene invocata, è l' Zebraistanza più derivata che viene invocata contro la zebraActionfunzione (passata come parametro), sebbene la funzione stessa utilizzi un tipo meno derivato.